diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index b0f7305a55b4..17fa19ccfcec 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -1594,6 +1594,8 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS runtime/ModuleMap.h runtime/ModuleProgramExecutable.h runtime/ModuleProgramExecutableInlines.h + runtime/ModuleGraphInstance.h + runtime/ModuleGraphInstanceInlines.h runtime/ModuleRegistryEntry.h runtime/NarrowingNumberPredictionFuzzerAgent.h runtime/NativeCallee.h diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt index 23315183aaba..c24c5b8055e6 100644 --- a/Source/JavaScriptCore/Sources.txt +++ b/Source/JavaScriptCore/Sources.txt @@ -1009,6 +1009,7 @@ runtime/ModuleGraphLoadingState.cpp runtime/ModuleLoadingContext.cpp runtime/ModuleLoaderPayload.cpp runtime/ModuleProgramExecutable.cpp +runtime/ModuleGraphInstance.cpp runtime/ModuleRegistryEntry.cpp runtime/NarrowingNumberPredictionFuzzerAgent.cpp runtime/NativeCallee.cpp diff --git a/Source/JavaScriptCore/builtins/BuiltinNames.h b/Source/JavaScriptCore/builtins/BuiltinNames.h index 51c5992c124c..e7e3f6a72e59 100644 --- a/Source/JavaScriptCore/builtins/BuiltinNames.h +++ b/Source/JavaScriptCore/builtins/BuiltinNames.h @@ -194,6 +194,7 @@ namespace JSC { macro(copyDataProperties) \ macro(cloneObject) \ macro(meta) \ + macro(moduleGraphInstance) \ macro(instanceFieldInitializer) \ macro(privateBrand) \ macro(privateClassBrand) \ diff --git a/Source/JavaScriptCore/bytecode/BytecodeList.rb b/Source/JavaScriptCore/bytecode/BytecodeList.rb index 08ad777dbbd9..c8f5e9f3c949 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeList.rb +++ b/Source/JavaScriptCore/bytecode/BytecodeList.rb @@ -522,6 +522,7 @@ }, metadata: { resolveType: ResolveType, # offset 4 + moduleImportSlot: unsigned, # ModuleVar: 1 + import slot ScopeOffset in the importing environment, 0 = none _0: { # offset 5 localScopeDepth: unsigned, globalLexicalBindingEpoch: unsigned, diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index 8896f2349b39..8f78aedb7146 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -607,6 +607,7 @@ bool CodeBlock::finishCreation(VM& vm, ScriptExecutable* ownerExecutable, Unlink metadata.m_resolveType = op.type; metadata.m_localScopeDepth = op.depth; + metadata.m_moduleImportSlot = op.moduleImportSlot; if (op.lexicalEnvironment) { if (op.type == ModuleVar) { // Keep the linked module environment strongly referenced. diff --git a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp index 7d149beffad8..f785cf5d0d79 100644 --- a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp +++ b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp @@ -10395,6 +10395,7 @@ void ByteCodeParser::parseBlock(unsigned limit) ResolveType resolveType; unsigned depth; + unsigned moduleImportSlot = 0; JSScope* constantScope = nullptr; JSCell* lexicalEnvironment = nullptr; SymbolTable* symbolTable = nullptr; @@ -10402,6 +10403,7 @@ void ByteCodeParser::parseBlock(unsigned limit) ConcurrentJSLocker locker(m_inlineStackTop->m_profiledBlock->m_lock); resolveType = metadata.m_resolveType; depth = metadata.m_localScopeDepth; + moduleImportSlot = metadata.m_moduleImportSlot; switch (resolveType) { case GlobalProperty: case GlobalVar: @@ -10455,11 +10457,46 @@ void ByteCodeParser::parseBlock(unsigned limit) break; } case ModuleVar: { - // Module environment is already strongly referenced by the CodeBlock. - set(bytecode.m_dst, weakJSConstant(lexicalEnvironment)); - // BytecodeUseDef reports m_scope as a use regardless of resolve type, - // so we need to keep it OSR-available even though LLInt won't read it. - addToGraph(Phantom, get(bytecode.m_scope)); + Node* localBase = get(bytecode.m_scope); + addToGraph(Phantom, localBase); + if (!moduleImportSlot) { + // No import slot (module graph instances off): the linked + // exporter environment is the only one there is. + set(bytecode.m_dst, weakJSConstant(lexicalEnvironment)); + break; + } + // Module graph instances: the exporter environment is the importing + // environment's import slot — a closure-variable-like load. It folds + // to the linked exporter environment while that is the exporting + // module's only environment (its symbol table's singleton; the + // watchpoint fires when an instance creates a second one), and to + // the slot's value when the importing environment is a constant here. + ScopeOffset slot(moduleImportSlot - 1); + if (lexicalEnvironment) { + SymbolTable* exporterSymbolTable = uncheckedDowncast(lexicalEnvironment)->symbolTable(); + if (exporterSymbolTable->singleton().inferredValue() == lexicalEnvironment) { + m_graph.watchpoints().addLazily(m_graph, exporterSymbolTable); + set(bytecode.m_dst, weakJSConstant(lexicalEnvironment)); + break; + } + } + if (JSScope* resolvedScope = localBase->dynamicCastConstant()) { + for (unsigned n = depth; n--;) + resolvedScope = resolvedScope->next(); + if (auto* importer = dynamicDowncast(resolvedScope)) { + // Import slots sit past the symbol table's scope size; index the storage directly. + if (JSValue exporter = importer->variables()[slot.offset()].get()) { + set(bytecode.m_dst, weakJSConstant(exporter.asCell())); + break; + } + } + } + for (unsigned n = depth; n--;) + localBase = addToGraph(SkipScope, localBase); + Node* exporter = addToGraph(GetClosureVar, OpInfo(slot.offset()), OpInfo(SpecObjectOther), localBase); + addToGraph(CheckNotEmpty, exporter); + addToGraph(Check, Edge(exporter, CellUse)); + set(bytecode.m_dst, exporter); break; } case ResolvedClosureVar: diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index 9d8c4ff2704a..45f17e543b4f 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -76,6 +76,7 @@ #include "MarkedSpaceInlines.h" #include "MarkingConstraintSet.h" #include "MegamorphicCache.h" +#include "ModuleGraphInstance.h" #include "ModuleLoadingContext.h" #include "ModuleProgramExecutable.h" #include "ModuleRegistryEntry.h" diff --git a/Source/JavaScriptCore/heap/Heap.h b/Source/JavaScriptCore/heap/Heap.h index 0d3d10e94047..c4d92ff6d179 100644 --- a/Source/JavaScriptCore/heap/Heap.h +++ b/Source/JavaScriptCore/heap/Heap.h @@ -274,6 +274,8 @@ class Heap; v(jsModuleRecordSpace, jsModuleRecordHeapCellType, JSModuleRecord) \ v(moduleRegistryEntrySpace, destructibleCellHeapCellType, ModuleRegistryEntry) \ v(moduleLoadingContextSpace, destructibleCellHeapCellType, ModuleLoadingContext) \ + v(moduleGraphInstanceSpace, destructibleObjectHeapCellType, ModuleGraphInstance) \ + v(moduleRecordInstanceSpace, destructibleCellHeapCellType, ModuleRecordInstance) \ v(sentinelSpace, cellHeapCellType, JSSentinel) \ v(syntheticModuleRecordSpace, syntheticModuleRecordHeapCellType, SyntheticModuleRecord) \ v(jsMicrotaskDispatcherSpace, destructibleCellHeapCellType, JSMicrotaskDispatcher) \ diff --git a/Source/JavaScriptCore/interpreter/CallFrame.cpp b/Source/JavaScriptCore/interpreter/CallFrame.cpp index 9a325c190752..04ff91cecd1a 100644 --- a/Source/JavaScriptCore/interpreter/CallFrame.cpp +++ b/Source/JavaScriptCore/interpreter/CallFrame.cpp @@ -31,6 +31,7 @@ #include "ExecutableAllocator.h" #include "InlineCallFrame.h" #include "JSCInlines.h" +#include "JSCallee.h" #include "JSWebAssemblyInstance.h" #include "JSWebAssemblyModule.h" #include "LLIntPCRanges.h" @@ -194,6 +195,42 @@ SUPPRESS_ASAN CallFrame* CallFrame::unsafeCallerFrame(EntryFrame*& currEntryFram return static_cast(unsafeCallerFrameOrEntryFrame()); } + +JSScope* CallFrame::callerScope(VM& vm) +{ + RELEASE_ASSERT(callee().isCell()); + JSScope* found = nullptr; + bool haveSkippedFirstFrame = false; + StackVisitor::visit(this, vm, [&](StackVisitor& visitor) { + if (!std::exchange(haveSkippedFirstFrame, true)) + return IterationStatus::Continue; + switch (visitor->codeType()) { + case StackVisitor::Frame::CodeType::Native: + case StackVisitor::Frame::CodeType::Wasm: + // Eval code runs in its caller's scope, but its callee is the global + // object's shared eval callee whose scope is only set while the eval is + // being entered: attribute to the frame that called eval instead. + case StackVisitor::Frame::CodeType::Eval: + return IterationStatus::Continue; + case StackVisitor::Frame::CodeType::Function: + case StackVisitor::Frame::CodeType::Module: + case StackVisitor::Frame::CodeType::Global: + break; + } + // The callee carries the scope its code was created with at every tier + // (the scope register is not materialized in optimized frames). + JSCell* calleeCell = visitor->callee().asCell(); + if (auto* function = dynamicDowncast(calleeCell)) { + if (!function->isHostFunction() && function->jsExecutable()->isPrivateBuiltinFunction()) + return IterationStatus::Continue; + found = function->scope(); + } else if (auto* callee = dynamicDowncast(calleeCell)) + found = callee->scope(); + return IterationStatus::Done; + }); + return found; +} + SourceOrigin CallFrame::callerSourceOrigin(VM& vm) { RELEASE_ASSERT(callee().isCell()); diff --git a/Source/JavaScriptCore/interpreter/CallFrame.h b/Source/JavaScriptCore/interpreter/CallFrame.h index 517d88a61d59..e51b8428b261 100644 --- a/Source/JavaScriptCore/interpreter/CallFrame.h +++ b/Source/JavaScriptCore/interpreter/CallFrame.h @@ -228,6 +228,11 @@ using JSInstruction = BaseInstruction; JS_EXPORT_PRIVATE CallFrame* callerFrame(EntryFrame*&) const; JS_EXPORT_PRIVATE SourceOrigin callerSourceOrigin(VM&); + // Module graph instances: the scope the calling JS code closes over (the + // module environment for module code; the callee's scope chain for + // functions), or null for native callers. Lets import() load into the + // caller's graph instance. + JS_EXPORT_PRIVATE JSScope* callerScope(VM&); static constexpr ptrdiff_t callerFrameOffset() { return OBJECT_OFFSETOF(CallerFrameAndPC, callerFrame); } diff --git a/Source/JavaScriptCore/interpreter/Interpreter.cpp b/Source/JavaScriptCore/interpreter/Interpreter.cpp index 32e6f65ca0af..b17456005573 100644 --- a/Source/JavaScriptCore/interpreter/Interpreter.cpp +++ b/Source/JavaScriptCore/interpreter/Interpreter.cpp @@ -73,6 +73,7 @@ #include "LLIntThunks.h" #include "LiteralParser.h" #include "MicrotaskCall.h" +#include "ModuleGraphInstance.h" #include "ModuleProgramCodeBlock.h" #include "NativeCallee.h" #include "ProgramCodeBlock.h" @@ -1708,6 +1709,11 @@ JSValue Interpreter::executeEval(EvalExecutable* eval, JSValue thisValue, JSScop } JSValue Interpreter::executeModuleProgram(JSModuleRecord* record, ModuleProgramExecutable* executable, JSGlobalObject* lexicalGlobalObject, JSModuleEnvironment* scope, JSValue sentValue, JSValue resumeMode) +{ + return executeModuleProgram(record, record, executable, lexicalGlobalObject, scope, sentValue, resumeMode); +} + +JSValue Interpreter::executeModuleProgram(JSModuleRecord* record, JSObject* generatorState, ModuleProgramExecutable* executable, JSGlobalObject* lexicalGlobalObject, JSModuleEnvironment* scope, JSValue sentValue, JSValue resumeMode) { VM& vm = this->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -1737,9 +1743,15 @@ JSValue Interpreter::executeModuleProgram(JSModuleRecord* record, ModuleProgramE RefPtr jitCode; ProtoCallFrame protoCallFrame; + auto stateField = [&]() -> WriteBarrier& { + if (auto* recordInstance = dynamicDowncast(generatorState)) + return recordInstance->internalField(ModuleRecordInstance::Field::State); + ASSERT(generatorState == record); + return record->internalField(JSModuleRecord::Field::State); + }; EncodedJSValue args[numberOfArguments] = { - JSValue::encode(record), - JSValue::encode(record->internalField(JSModuleRecord::Field::State).get()), + JSValue::encode(generatorState), + JSValue::encode(stateField().get()), JSValue::encode(sentValue), JSValue::encode(resumeMode), JSValue::encode(scope), @@ -1767,7 +1779,7 @@ JSValue Interpreter::executeModuleProgram(JSModuleRecord* record, ModuleProgramE protoCallFrame.init(codeBlock, globalObject, callee, jsUndefined(), nullptr, numberOfArguments + 1, args); } - record->internalField(JSModuleRecord::Field::State).set(vm, record, jsNumber(static_cast(JSModuleRecord::State::Executing))); + stateField().set(vm, generatorState, jsNumber(static_cast(JSModuleRecord::State::Executing))); } // Execute the code: diff --git a/Source/JavaScriptCore/interpreter/Interpreter.h b/Source/JavaScriptCore/interpreter/Interpreter.h index ca093c05f926..27ffd1f290ac 100644 --- a/Source/JavaScriptCore/interpreter/Interpreter.h +++ b/Source/JavaScriptCore/interpreter/Interpreter.h @@ -161,6 +161,9 @@ using JSOrWasmInstruction = Variantas(); const Identifier& ident = codeBlock->identifier(bytecode.m_var); JSScope* environment = callFrame->uncheckedR(bytecode.m_scope).Register::scope(); + auto& metadata = bytecode.metadata(codeBlock); + + if (metadata.m_resolveType == ModuleVar) { + JSObject* result = JSModuleEnvironment::resolveModuleVarScope(globalObject, environment, metadata.m_localScopeDepth, uncheckedDowncast(metadata.m_lexicalEnvironment.get())); + OPERATION_RETURN_IF_EXCEPTION(scope, encodedJSValue()); + OPERATION_RETURN(scope, JSValue::encode(result)); + } + JSObject* resolvedScope = JSScope::resolve(globalObject, environment, ident); // Proxy can throw an error here, e.g. Proxy in with statement's @unscopables. OPERATION_RETURN_IF_EXCEPTION(scope, encodedJSValue()); - auto& metadata = bytecode.metadata(codeBlock); ResolveType resolveType = metadata.m_resolveType; - // ModuleVar does not keep the scope register value alive in DFG. - ASSERT(resolveType != ModuleVar); - switch (resolveType) { case GlobalProperty: case GlobalPropertyWithVarInjectionChecks: diff --git a/Source/JavaScriptCore/jit/JITPropertyAccess.cpp b/Source/JavaScriptCore/jit/JITPropertyAccess.cpp index 3ef44496a8af..6f0a91a3a351 100644 --- a/Source/JavaScriptCore/jit/JITPropertyAccess.cpp +++ b/Source/JavaScriptCore/jit/JITPropertyAccess.cpp @@ -896,9 +896,34 @@ void JIT::emit_op_resolve_scope(const JSInstruction* currentInstruction) // If we profile certain resolve types, we're guaranteed all linked code will have the same // resolve type. - if (profiledResolveType == ModuleVar) - loadPtrFromMetadata(bytecode, Metadata::offsetOfLexicalEnvironment(), returnValueGPR); - else if (profiledResolveType == ClosureVar) { + if (profiledResolveType == ModuleVar) { + if (!Options::useModuleGraphInstances()) { + // The exporter environment is a link-time constant. + loadPtrFromMetadata(bytecode, Metadata::offsetOfLexicalEnvironment(), returnValueGPR); + } else { + // Module graph instances: the import slot index (like the exporter + // environment) is a per-CodeBlock link-time value, and baseline code + // is shared between CodeBlocks of one UnlinkedCodeBlock, so read it + // from metadata. Slot 0: constant environment. Otherwise walk to the + // importing module environment (a lexical depth) and load its import + // slot; an unfilled slot goes to the slow path. + load32FromMetadata(bytecode, Metadata::offsetOfModuleImportSlot(), scratch1GPR); + Jump hasImportSlot = branchTest32(NonZero, scratch1GPR); + loadPtrFromMetadata(bytecode, Metadata::offsetOfLexicalEnvironment(), returnValueGPR); + Jump done = jump(); + hasImportSlot.link(this); + emitGetVirtualRegister(scope, scopeGPR); + static_assert(scopeGPR == returnValueGPR); + unsigned localScopeDepth = bytecode.metadata(m_profiledCodeBlock).m_localScopeDepth; + for (unsigned index = 0; index < localScopeDepth; ++index) + loadPtr(Address(returnValueGPR, JSScope::offsetOfNext()), returnValueGPR); + static_assert(sizeof(WriteBarrier) == 8); + sub32(TrustedImm32(1), scratch1GPR); + load64(BaseIndex(returnValueGPR, scratch1GPR, TimesEight, JSLexicalEnvironment::offsetOfVariables()), returnValueGPR); + addSlowCase(branchIfEmpty(returnValueGPR)); + done.link(this); + } + } else if (profiledResolveType == ClosureVar) { emitGetVirtualRegister(scope, scopeGPR); static_assert(scopeGPR == returnValueGPR); unsigned localScopeDepth = bytecode.metadata(m_profiledCodeBlock).m_localScopeDepth; @@ -1093,10 +1118,10 @@ MacroAssemblerCodeRef JIT::generateOpResolveScopeThunk(VM& vm) emitResolveClosure(needsVarInjectionChecks(resolveType)); break; case Dynamic: + case ModuleVar: slowCase.append(jit.jump()); break; case ResolvedClosureVar: - case ModuleVar: case UnresolvedProperty: case UnresolvedPropertyWithVarInjectionChecks: RELEASE_ASSERT_NOT_REACHED(); diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm index 57d8b0403d8e..7bf8870e3236 100644 --- a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm +++ b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm @@ -2849,6 +2849,25 @@ llintOpWithMetadata(op_resolve_scope, OpResolveScope, macro (size, get, dispatch .rModuleVar: bineq t0, ModuleVar, .rGlobalPropertyWithVarInjectionChecks + # With module graph instances the exporting environment is per instance: it + # is the importing environment's import slot (walk localScopeDepth to the + # importing module environment, load the slot; empty = not filled yet). + loadi OpResolveScope::Metadata::m_moduleImportSlot[t5], t1 + btiz t1, .rModuleVarConstant + loadi OpResolveScope::Metadata::m_localScopeDepth[t5], t2 + get(m_scope, t0) + loadq [cfr, t0, 8], t0 + btiz t2, .rModuleVarLoad +.rModuleVarWalk: + loadp JSScope::m_next[t0], t0 + subi 1, t2 + btinz t2, .rModuleVarWalk +.rModuleVarLoad: + subi 1, t1 + loadq JSLexicalEnvironment_variables[t0, t1, 8], t0 + btqz t0, .rDynamic + return(t0) +.rModuleVarConstant: returnConstantScope() .rGlobalPropertyWithVarInjectionChecks: diff --git a/Source/JavaScriptCore/lol/LOLJIT.cpp b/Source/JavaScriptCore/lol/LOLJIT.cpp index 8b5f990d0396..aefe4c1d9cd8 100644 --- a/Source/JavaScriptCore/lol/LOLJIT.cpp +++ b/Source/JavaScriptCore/lol/LOLJIT.cpp @@ -3597,9 +3597,28 @@ void LOLJIT::emit_op_resolve_scope(const JSInstruction* currentInstruction) // If we profile certain resolve types, we're guaranteed all linked code will have the same // resolve type. - if (profiledResolveType == ModuleVar) - loadPtrFromMetadata(bytecode, Metadata::offsetOfLexicalEnvironment(), destRegs.payloadGPR()); - else if (profiledResolveType == ClosureVar) { + if (profiledResolveType == ModuleVar) { + if (!Options::useModuleGraphInstances()) + loadPtrFromMetadata(bytecode, Metadata::offsetOfLexicalEnvironment(), destRegs.payloadGPR()); + else { + // Module graph instances (see JIT::emit_op_resolve_scope): slot index + // from metadata; 0 = constant environment, otherwise walk to the + // importing module environment and load its import slot (empty = slow). + load32FromMetadata(bytecode, Metadata::offsetOfModuleImportSlot(), s_scratch); + Jump hasImportSlot = branchTest32(NonZero, s_scratch); + loadPtrFromMetadata(bytecode, Metadata::offsetOfLexicalEnvironment(), destRegs.payloadGPR()); + Jump done = jump(); + hasImportSlot.link(this); + move(scopeRegs.payloadGPR(), destRegs.payloadGPR()); + unsigned localScopeDepth = bytecode.metadata(m_profiledCodeBlock).m_localScopeDepth; + for (unsigned index = 0; index < localScopeDepth; ++index) + loadPtr(Address(destRegs.payloadGPR(), JSScope::offsetOfNext()), destRegs.payloadGPR()); + sub32(TrustedImm32(1), s_scratch); + load64(BaseIndex(destRegs.payloadGPR(), s_scratch, TimesEight, JSLexicalEnvironment::offsetOfVariables()), destRegs.payloadGPR()); + addSlowCase(branchIfEmpty(destRegs.payloadGPR())); + done.link(this); + } + } else if (profiledResolveType == ClosureVar) { move(scopeRegs.payloadGPR(), destRegs.payloadGPR()); unsigned localScopeDepth = bytecode.metadata(m_profiledCodeBlock).m_localScopeDepth; if (localScopeDepth < 8) { @@ -3822,10 +3841,10 @@ MacroAssemblerCodeRef LOLJIT::generateOpResolveScopeThunk(VM& vm emitResolveClosure(needsVarInjectionChecks(resolveType)); break; case Dynamic: + case ModuleVar: slowCase.append(jit.jump()); break; case ResolvedClosureVar: - case ModuleVar: case UnresolvedProperty: case UnresolvedPropertyWithVarInjectionChecks: RELEASE_ASSERT_NOT_REACHED(); diff --git a/Source/JavaScriptCore/lol/LOLJITOperations.cpp b/Source/JavaScriptCore/lol/LOLJITOperations.cpp index e786d8175331..b70fdda4e751 100644 --- a/Source/JavaScriptCore/lol/LOLJITOperations.cpp +++ b/Source/JavaScriptCore/lol/LOLJITOperations.cpp @@ -37,6 +37,7 @@ #include "DFGOSREntry.h" #include "DFGThunks.h" #include "Debugger.h" +#include "JSModuleEnvironment.h" #include "EnsureStillAliveHere.h" #include "ExceptionFuzz.h" #include "FrameTracers.h" @@ -85,17 +86,23 @@ JSC_DEFINE_JIT_OPERATION(operationResolveScopeForLOL, EncodedJSValue, (CallFrame const JSInstruction* pc = codeBlock->instructionAt(BytecodeIndex(bytecodeOffset)); auto bytecode = pc->as(); + auto& metadata = bytecode.metadata(codeBlock); + + // ModuleVar reaches here only with module graph instances (an unfilled + // import slot); see slow_path_resolve_scope. + if (metadata.m_resolveType == ModuleVar) { + JSObject* result = JSModuleEnvironment::resolveModuleVarScope(globalObject, environment, metadata.m_localScopeDepth, uncheckedDowncast(metadata.m_lexicalEnvironment.get())); + OPERATION_RETURN_IF_EXCEPTION(scope, encodedJSValue()); + OPERATION_RETURN(scope, JSValue::encode(result)); + } + const Identifier& ident = codeBlock->identifier(bytecode.m_var); JSObject* resolvedScope = JSScope::resolve(globalObject, environment, ident); // Proxy can throw an error here, e.g. Proxy in with statement's @unscopables. OPERATION_RETURN_IF_EXCEPTION(scope, encodedJSValue()); - auto& metadata = bytecode.metadata(codeBlock); ResolveType resolveType = metadata.m_resolveType; - // ModuleVar does not keep the scope register value alive in DFG. - ASSERT(resolveType != ModuleVar); - switch (resolveType) { case GlobalProperty: case GlobalPropertyWithVarInjectionChecks: diff --git a/Source/JavaScriptCore/parser/SourceProvider.h b/Source/JavaScriptCore/parser/SourceProvider.h index cbb77358b111..e023f18fe800 100644 --- a/Source/JavaScriptCore/parser/SourceProvider.h +++ b/Source/JavaScriptCore/parser/SourceProvider.h @@ -211,6 +211,13 @@ class StringSourceProvider : public SourceProvider { return adoptRef(*new SyntheticSourceProvider(nullptr, WTF::move(generator), sourceOrigin, WTF::move(sourceURL))); } + // Module graph instances: a generator that yields a fresh module per call + // and honours JSGlobalObject::currentGraphInstanceForLoading() (e.g. a + // CommonJS module evaluated in that graph's cache) — such records get + // their own environment per graph and a lazily produced primary. + void setRegeneratesPerGraphInstance(bool value) { m_regeneratesPerGraphInstance = value; } + bool regeneratesPerGraphInstance() const { return m_regeneratesPerGraphInstance; } + unsigned hash() const final { return m_source.impl()->hash(); @@ -243,6 +250,7 @@ class StringSourceProvider : public SourceProvider { String m_source; SyntheticSourceGenerator m_generator; LazySyntheticSourceGenerator m_lazyGenerator; + bool m_regeneratesPerGraphInstance { false }; }; #if ENABLE(WEBASSEMBLY) diff --git a/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp b/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp index 5c5139235daf..d863c5ef8a7b 100644 --- a/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp @@ -35,6 +35,7 @@ #include "JSModuleLoader.h" #include "JSModuleNamespaceObject.h" #include "JSModuleRecord.h" +#include "ModuleGraphInstance.h" #include "JSPromise.h" #if USE(BUN_JSC_ADDITIONS) #include "InternalFieldTuple.h" @@ -105,6 +106,8 @@ void AbstractModuleRecord::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_topLevelCapability); visitor.append(thisObject->m_asyncCapability); Locker locker { thisObject->cellLock() }; + for (auto& record : thisObject->m_importedRecords) + visitor.append(record); visitor.append(thisObject->m_asyncParentModules.begin(), thisObject->m_asyncParentModules.end()); for (const auto& [key, loadedModule] : thisObject->m_loadedModules) visitor.append(loadedModule.m_module); @@ -834,6 +837,99 @@ auto AbstractModuleRecord::resolveExport(JSGlobalObject* globalObject, const Ide RELEASE_AND_RETURN(scope, resolveExportImpl(globalObject, ResolveQuery(this, exportName.impl()))); } + +JSModuleEnvironment* AbstractModuleRecord::graphInstanceEnvironment(JSGlobalObject* globalObject, ModuleGraphInstance* instance, bool createForSynthetic) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (JSModuleEnvironment* existing = instance->environment(this)) + return existing; + if (!createForSynthetic) + return nullptr; + if (instance->isCleared()) { + throwTypeError(globalObject, scope, "Module graph instance was disposed"_s); + return nullptr; + } + auto* synthetic = dynamicDowncast(this); + if (!synthetic || !synthetic->hasPerGraphInstanceState()) + return nullptr; + JSModuleEnvironment* environment = nullptr; + { + JSGlobalObject::GraphInstanceLoadingScope loading(globalObject, instance); + environment = synthetic->createGraphInstanceEnvironment(globalObject); + } + RETURN_IF_EXCEPTION(scope, nullptr); + // The provider ran host code: the instance may have been disposed, or this + // record instantiated into it re-entrantly (add() then returns that one). + if (instance->isCleared()) { + throwTypeError(globalObject, scope, "Module graph instance was disposed"_s); + return nullptr; + } + return instance->add(vm, this, environment)->environment(); +} + +// GetModuleNamespace for the record as instantiated in `instance`: one namespace +// object per (instance, module), cached in the instance environment's +// *namespace* binding (deferred namespaces on the ModuleRecordInstance). +// Records the instance shares with the primary graph answer the primary's. +JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject* globalObject, ModuleGraphInstance* instance, ModulePhase phase) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!instance) + RELEASE_AND_RETURN(scope, getModuleNamespace(globalObject, phase)); + if (instance->isCleared()) { + throwTypeError(globalObject, scope, "Module graph instance was disposed"_s); + return nullptr; + } + JSModuleEnvironment* environment = graphInstanceEnvironment(globalObject, instance, true); + RETURN_IF_EXCEPTION(scope, nullptr); + if (!environment) + RELEASE_AND_RETURN(scope, getModuleNamespace(globalObject, phase)); + ModuleRecordInstance* recordInstance = instance->recordInstance(this); + ASSERT(recordInstance); + if (phase == ModulePhase::Defer) { + if (JSModuleNamespaceObject* deferred = recordInstance->deferredNamespaceObject()) + return deferred; + } + ScopeOffset namespaceOffset; + { + SymbolTable* symbolTable = environment->symbolTable(); + ConcurrentJSLocker locker(symbolTable->m_lock); + auto iterator = symbolTable->find(locker, vm.propertyNames->starNamespacePrivateName.impl()); + ASSERT(iterator != symbolTable->end(locker)); + namespaceOffset = iterator->value.scopeOffset(); + } + if (phase != ModulePhase::Defer) { + JSValue cached = environment->variableAt(namespaceOffset).get(); + if (cached && cached.isCell()) { + if (auto* namespaceObject = dynamicDowncast(cached.asCell())) + return namespaceObject; + } + } + // Same exported names as the primary namespace (which caches the resolutions). + JSModuleNamespaceObject* primary = getModuleNamespace(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + PropertyNameArrayBuilder names(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + JSModuleNamespaceObject::getOwnPropertyNames(primary, globalObject, names, DontEnumPropertiesMode::Include); + RETURN_IF_EXCEPTION(scope, nullptr); + Vector> resolutions; + for (auto& name : names) { + Resolution resolution = resolveExport(globalObject, name); + RETURN_IF_EXCEPTION(scope, nullptr); + if (resolution.type == Resolution::Type::Resolved) + resolutions.append({ name, resolution }); + } + auto* object = JSModuleNamespaceObject::create(globalObject, globalObject->moduleNamespaceObjectStructure(), this, WTF::move(resolutions), true, phase == ModulePhase::Defer); + RETURN_IF_EXCEPTION(scope, nullptr); + object->setGraphInstance(vm, instance, environment); + if (phase == ModulePhase::Defer) + recordInstance->setDeferredNamespaceObject(vm, object); + else + environment->variableAt(namespaceOffset).set(vm, environment, object); + return object; +} + JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject* globalObject, ModulePhase phase, bool shouldPreventExtensions) { VM& vm = globalObject->vm(); @@ -986,7 +1082,7 @@ JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject } // https://tc39.es/proposal-defer-import-eval/#sec-GatherAsynchronousTransitiveDependencies -void AbstractModuleRecord::gatherAsynchronousTransitiveDependencies(OrderedHashSet& result, UncheckedKeyHashSet& seen) +void AbstractModuleRecord::gatherAsynchronousTransitiveDependencies(OrderedHashSet& result, UncheckedKeyHashSet& seen, ModuleGraphInstance* instance) { // The spec text is recursive; we use an explicit work list to avoid native stack overflow on // deep graphs. Children are pushed in reverse to preserve the spec's pre-order discovery order. @@ -1003,7 +1099,7 @@ void AbstractModuleRecord::gatherAsynchronousTransitiveDependencies(OrderedHashS if (!cyclic) continue; // 6. If module.[[Status]] is either EVALUATING or IsModuleSCCEvaluated(module), return result. - if (cyclic->status() == CyclicModuleRecord::Status::Evaluating || cyclic->isSCCEvaluated()) + if (cyclic->status(instance) == CyclicModuleRecord::Status::Evaluating || cyclic->isSCCEvaluated(instance)) continue; // 7. If module.[[HasTLA]] is true, then if (cyclic->hasTLA()) { @@ -1024,7 +1120,7 @@ void AbstractModuleRecord::gatherAsynchronousTransitiveDependencies(OrderedHashS } // https://tc39.es/proposal-defer-import-eval/#sec-ReadyForSyncExecution -bool AbstractModuleRecord::readyForSyncExecution() +bool AbstractModuleRecord::readyForSyncExecution(ModuleGraphInstance* instance) { // The spec text is recursive; we use an explicit work list to avoid native stack overflow on deep graphs. UncheckedKeyHashSet seen; @@ -1041,16 +1137,16 @@ bool AbstractModuleRecord::readyForSyncExecution() if (!seen.add(module).isNewEntry) continue; // 5. If IsModuleSCCEvaluated(module), return true. - if (cyclic->isSCCEvaluated()) + if (cyclic->isSCCEvaluated(instance)) continue; // 6. If module.[[Status]] is either EVALUATING or EVALUATING-ASYNC, return false. - if (cyclic->status() == CyclicModuleRecord::Status::Evaluating || cyclic->status() == CyclicModuleRecord::Status::EvaluatingAsync) + if (cyclic->status(instance) == CyclicModuleRecord::Status::Evaluating || cyclic->status(instance) == CyclicModuleRecord::Status::EvaluatingAsync) return false; // 7. Assert: module.[[Status]] is LINKED or EVALUATED. // EVALUATED is reachable for a module whose own body has run inside a cycle that is still // awaiting; the walk below then reaches its EVALUATING-ASYNC cycle root and returns false. // https://github.com/tc39/proposal-defer-import-eval/issues/86 - ASSERT(cyclic->status() == CyclicModuleRecord::Status::Linked || cyclic->status() == CyclicModuleRecord::Status::Evaluated); + ASSERT(cyclic->status(instance) == CyclicModuleRecord::Status::Linked || cyclic->status(instance) == CyclicModuleRecord::Status::Evaluated); // 8. If module.[[HasTLA]] is true, return false. if (cyclic->hasTLA()) return false; @@ -1065,17 +1161,30 @@ bool AbstractModuleRecord::readyForSyncExecution() } // https://tc39.es/proposal-defer-import-eval/#sec-EvaluateModuleSync -void AbstractModuleRecord::evaluateSync(JSGlobalObject* globalObject) +void AbstractModuleRecord::evaluateSync(JSGlobalObject* globalObject, ModuleGraphInstance* instance) { + ModuleGraphInstance::BusyScope busy(globalObject, instance); VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + if (instance && instance->isCleared()) { + throwTypeError(globalObject, scope, "Module graph instance was disposed"_s); + return; + } // 1. If ReadyForSyncExecution(module) is false, throw a TypeError exception. - if (!readyForSyncExecution()) { + if (!readyForSyncExecution(instance)) { throwTypeError(globalObject, scope, "Unable to synchronously evaluate deferred module"_s); return; } // 2. Let promise be ! module.Evaluate(). - JSPromise* promise = evaluate(globalObject); + JSPromise* promise = nullptr; + if (auto* cyclic = dynamicDowncast(this); cyclic && instance) { +#if USE(BUN_JSC_ADDITIONS) + promise = cyclic->evaluate(globalObject, -1, nullptr, instance); +#else + promise = cyclic->evaluate(globalObject, instance); +#endif + } else + promise = evaluate(globalObject); RETURN_IF_EXCEPTION(scope, void()); // 3. Assert: promise.[[PromiseState]] is either FULFILLED or REJECTED. ASSERT(promise->status() != JSPromise::Status::Pending); @@ -1090,6 +1199,19 @@ void AbstractModuleRecord::evaluateSync(JSGlobalObject* globalObject) // 5. Return UNUSED. } +void AbstractModuleRecord::setImportedRecords(VM& vm, const Vector& records) +{ + ASSERT(!m_importedRecordsSet || m_importedRecords.size() == records.size()); + if (std::exchange(m_importedRecordsSet, true)) + return; + auto importedRecords = WTF::map(records, [&](AbstractModuleRecord* record) { + return WriteBarrier(vm, this, record); + }); + // The concurrent marker iterates m_importedRecords under the cell lock. + Locker locker { cellLock() }; + m_importedRecords = WTF::move(importedRecords); +} + JSPromise* AbstractModuleRecord::asyncCapability() const { return m_asyncCapability.get(); @@ -1212,8 +1334,10 @@ static void checkSafeToRecurse(JSGlobalObject* globalObject, ThrowScope& scope) } #if USE(BUN_JSC_ADDITIONS) -static bool importPromiseGatesAsyncDependency(JSPromise* importPromise, CyclicModuleRecord* dependency) +static bool importPromiseGatesAsyncDependency(JSPromise* importPromise, CyclicModuleRecord* dependency, ModuleGraphInstance* instance) { + // In a graph instance the [[AsyncParentModules]] of a record live on its + // ModuleRecordInstance; the primary graph's are on the record. auto resumesDependency = [&](AbstractModuleRecord* module) -> bool { UncheckedKeyHashSet seen; Vector work; @@ -1224,11 +1348,23 @@ static bool importPromiseGatesAsyncDependency(JSPromise* importPromise, CyclicMo return true; if (!seen.add(current).isNewEntry) continue; - for (auto& parent : current->asyncParentModules()) + ModuleRecordInstance* state = instance ? instance->recordInstance(current) : nullptr; + for (auto& parent : state ? state->asyncParentModules() : current->asyncParentModules()) work.append(parent.get()); } return false; }; + // An AsyncModuleExecutionResume reaction's driver is the record in the + // primary graph and the ModuleRecordInstance in a graph instance; only one + // of this evaluation's instance can gate the dependency. + auto moduleForDriver = [&](JSCell* driver) -> AbstractModuleRecord* { + if (!driver) + return nullptr; + if (auto* recordInstance = dynamicDowncast(driver)) + return recordInstance->graphInstance() == instance ? recordInstance->record() : nullptr; + auto* module = dynamicDowncast(driver); + return module && !instance ? module : nullptr; + }; auto cellOf = [](JSValue value) -> JSCell* { if (value.isEmpty() || !value.isCell()) @@ -1263,7 +1399,7 @@ static bool importPromiseGatesAsyncDependency(JSPromise* importPromise, CyclicMo work.append(promise); else if (auto* generator = dynamicDowncast(cell)) follow(generator->context()); - else if (auto* module = dynamicDowncast(cell)) + else if (AbstractModuleRecord* module = moduleForDriver(cell)) found = resumesDependency(module); }; @@ -1315,6 +1451,9 @@ static bool importPromiseGatesAsyncDependency(JSPromise* importPromise, CyclicMo case InternalMicrotask::DynamicImportEvaluateSettled: case InternalMicrotask::DynamicImportDeferLoadSettled: case InternalMicrotask::DynamicImportDeferDependencySettled: + case InternalMicrotask::ModuleGraphInstanceLoadSettled: + case InternalMicrotask::ModuleGraphInstanceEvaluateSettled: + case InternalMicrotask::ModuleGraphInstanceDependencySettled: follow(cell); break; default: @@ -1339,9 +1478,9 @@ static bool importPromiseGatesAsyncDependency(JSPromise* importPromise, CyclicMo #endif #if USE(BUN_JSC_ADDITIONS) -unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObject, Vector& stack, unsigned index, int64_t referrerAsyncOrder, JSPromise* dynamicImportPromise) +unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObject, Vector& stack, unsigned index, int64_t referrerAsyncOrder, JSPromise* dynamicImportPromise, ModuleGraphInstance* instance) #else -unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObject, Vector& stack, unsigned index) +unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObject, Vector& stack, unsigned index, ModuleGraphInstance* instance) #endif { // InnerModuleEvaluation(module, stack, index) @@ -1357,15 +1496,19 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec // 1. If module is not a Cyclic Module Record, then if (!module) { // 1.a. Perform ? EvaluateModuleSync(module). - evaluateModuleSync(globalObject); + // A record with its own environment in the instance was given its values + // when that environment was created; only records the instance shares + // with the primary graph evaluate (the primary). + if (!instance || !instance->recordInstance(this)) + evaluateModuleSync(globalObject); RETURN_IF_EXCEPTION(scope, invalid); // 1.b. Return index. return index; } // 2. If module.[[Status]] is either EVALUATING-ASYNC or EVALUATED, then - if (auto status = module->status(); status == Status::EvaluatingAsync || status == Status::Evaluated) { + if (auto status = module->status(instance); status == Status::EvaluatingAsync || status == Status::Evaluated) { // 2.a. If module.[[EvaluationError]] is EMPTY, return index. - JSValue evaluationError = module->evaluationError(); + JSValue evaluationError = module->evaluationError(instance); if (!evaluationError) RELEASE_AND_RETURN(scope, index); // 2.b. Otherwise, return ? module.[[EvaluationError]]. @@ -1373,18 +1516,18 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec return invalid; } // 3. If module.[[Status]] is EVALUATING, return index. - if (module->status() == Status::Evaluating) + if (module->status(instance) == Status::Evaluating) RELEASE_AND_RETURN(scope, index); // 4. Assert: module.[[Status]] is LINKED. - ASSERT(module->status() == Status::Linked); + ASSERT(module->status(instance) == Status::Linked); // 5. Set module.[[Status]] to EVALUATING. - module->setStatus(Status::Evaluating); + module->setStatus(instance, Status::Evaluating); // 6. Let moduleIndex be index. unsigned moduleIndex = index; // 7. Set module.[[DFSAncestorIndex]] to index. - module->setDFSAncestorIndex(index); + module->setDFSAncestorIndex(instance, index); // 8. Set module.[[PendingAsyncDependencies]] to 0. - module->setPendingAsyncDependencies(0); + module->setPendingAsyncDependencies(instance, 0); // 9. Set index to index + 1. ++index; // 10. Append module to stack. @@ -1402,7 +1545,7 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec // 11.b.ii. For each Module Record additionalModule of additionalModules, do // 11.b.ii.1. If evaluationList does not contain additionalModule, then append additionalModule to evaluationList. UncheckedKeyHashSet seen; - requiredModule->gatherAsynchronousTransitiveDependencies(evaluationList, seen); + requiredModule->gatherAsynchronousTransitiveDependencies(evaluationList, seen, instance); } else { // 11.c. Else if evaluationList does not contain requiredModule, then // 11.c.i. Append requiredModule to evaluationList. @@ -1415,9 +1558,9 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec RETURN_IF_EXCEPTION(scope, invalid); // 12.a. Set index to ? InnerModuleEvaluation(requiredModule, stack, index). #if USE(BUN_JSC_ADDITIONS) - unsigned result = requiredModule->innerModuleEvaluation(globalObject, stack, index, referrerAsyncOrder, dynamicImportPromise); + unsigned result = requiredModule->innerModuleEvaluation(globalObject, stack, index, referrerAsyncOrder, dynamicImportPromise, instance); #else - unsigned result = requiredModule->innerModuleEvaluation(globalObject, stack, index); + unsigned result = requiredModule->innerModuleEvaluation(globalObject, stack, index, instance); #endif RETURN_IF_EXCEPTION(scope, invalid); index = result; @@ -1436,42 +1579,42 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec // its DFSAncestorIndex into our inner SCC would taint the SCC // linearization. The outer DFS owns the module's evaluation // lifecycle; our inner pass treats it as a satisfied dependency. - bool depInOuterSCC = cyclic->status() == Status::Evaluating && !stack.contains(requiredModule); + bool depInOuterSCC = cyclic->status(instance) == Status::Evaluating && !stack.contains(requiredModule); if (!depInOuterSCC) { #endif // 12.b.i. Assert: requiredModule.[[Status]] is one of EVALUATING, EVALUATING-ASYNC, or EVALUATED. - ASSERT(cyclic->status() == Status::Evaluating || cyclic->status() == Status::EvaluatingAsync || cyclic->status() == Status::Evaluated); + ASSERT(cyclic->status(instance) == Status::Evaluating || cyclic->status(instance) == Status::EvaluatingAsync || cyclic->status(instance) == Status::Evaluated); // 12.b.ii. Assert: requiredModule.[[Status]] is EVALUATING if and only if stack contains requiredModule. - ASSERT(stack.contains(requiredModule) == (cyclic->status() == Status::Evaluating)); + ASSERT(stack.contains(requiredModule) == (cyclic->status(instance) == Status::Evaluating)); // 12.b.iii. If requiredModule.[[Status]] is EVALUATING, then - if (cyclic->status() == Status::Evaluating) { + if (cyclic->status(instance) == Status::Evaluating) { // 12.b.iii.1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], requiredModule.[[DFSAncestorIndex]]). - module->setDFSAncestorIndex(std::min(module->dfsAncestorIndex(), cyclic->dfsAncestorIndex())); + module->setDFSAncestorIndex(instance, std::min(module->dfsAncestorIndex(instance), cyclic->dfsAncestorIndex(instance))); // 12.b.iv. Else, } else { // 12.b.iv.1. Set requiredModule to requiredModule.[[CycleRoot]]. - cyclic = requiredModule->cycleRoot(); + cyclic = cyclic->cycleRoot(instance); requiredModule = cyclic; // 12.b.iv.2. Assert: requiredModule.[[Status]] is either EVALUATING-ASYNC or EVALUATED. - ASSERT(cyclic->status() == Status::EvaluatingAsync || cyclic->status() == Status::Evaluated); + ASSERT(cyclic->status(instance) == Status::EvaluatingAsync || cyclic->status(instance) == Status::Evaluated); // 12.b.iv.3. If requiredModule.[[EvaluationError]] is not empty, return ? requiredModule.[[EvaluationError]]. - if (JSValue error = cyclic->evaluationError()) { + if (JSValue error = cyclic->evaluationError(instance)) { scope.throwException(globalObject, error); return invalid; } } // 12.b.v. If requiredModule.[[AsyncEvaluationOrder]] is an integer, then - if (cyclic->asyncEvaluationOrder().hasOrder()) { + if (cyclic->asyncEvaluationOrder(instance).hasOrder()) { #if USE(BUN_JSC_ADDITIONS) // referrerAsyncOrder covers an import() whose promise reaches the suspended referrer only through native code (an HTTP round trip, a captured resolver), where the walk cannot follow. - bool deadlocks = cyclic->asyncEvaluationOrder().order() == referrerAsyncOrder - || (dynamicImportPromise && importPromiseGatesAsyncDependency(dynamicImportPromise, cyclic)); + bool deadlocks = cyclic->asyncEvaluationOrder(instance).order() == referrerAsyncOrder + || (dynamicImportPromise && importPromiseGatesAsyncDependency(dynamicImportPromise, cyclic, instance)); if (!deadlocks) { #endif // 12.b.v.1. Set module.[[PendingAsyncDependencies]] to module.[[PendingAsyncDependencies]] + 1. - module->setPendingAsyncDependencies(module->pendingAsyncDependencies().value() + 1); + module->setPendingAsyncDependencies(instance, module->pendingAsyncDependencies(instance).value() + 1); // 12.b.v.2. Append module to requiredModule.[[AsyncParentModules]]. - cyclic->appendAsyncParentModule(vm, module); + cyclic->appendAsyncParentModule(vm, instance, module); #if USE(BUN_JSC_ADDITIONS) } #endif @@ -1482,29 +1625,29 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec } } // 12. If module.[[PendingAsyncDependencies]] > 0 or module.[[HasTLA]] is true, then - if (module->pendingAsyncDependencies() > 0 || module->hasTLA()) { + if (module->pendingAsyncDependencies(instance) > 0 || module->hasTLA()) { // 12.a. Assert: module.[[AsyncEvaluationOrder]] is UNSET. - ASSERT(module->asyncEvaluationOrder().isUnset()); + ASSERT(module->asyncEvaluationOrder(instance).isUnset()); // 12.b. Set module.[[AsyncEvaluationOrder]] to IncrementModuleAsyncEvaluationCount(). - module->setAsyncEvaluationOrder(vm.incrementModuleAsyncEvaluationCount()); + module->setAsyncEvaluationOrder(instance, vm.incrementModuleAsyncEvaluationCount()); // 12.c. If module.[[PendingAsyncDependencies]] = 0, perform ExecuteAsyncModule(module). - if (std::optional deps = module->pendingAsyncDependencies(); deps && !*deps) { - module->executeAsync(globalObject); + if (std::optional deps = module->pendingAsyncDependencies(instance); deps && !*deps) { + module->executeAsync(globalObject, instance); RETURN_IF_EXCEPTION(scope, invalid); } // 13. Else, } else { // 13.a. Perform ? module.ExecuteModule(). - module->execute(globalObject); + module->execute(globalObject, nullptr, instance); RETURN_IF_EXCEPTION(scope, invalid); } // 14. Assert: module occurs exactly once in stack. ASSERT(stack.contains(module)); ASSERT(stack.find(module) == stack.reverseFind(module)); // 15. Assert: module.[[DFSAncestorIndex]] <= moduleIndex. - ASSERT(module->dfsAncestorIndex() <= moduleIndex); + ASSERT(module->dfsAncestorIndex(instance) <= moduleIndex); // 16. If module.[[DFSAncestorIndex]] = moduleIndex, then - if (module->dfsAncestorIndex() == moduleIndex) { + if (module->dfsAncestorIndex(instance) == moduleIndex) { // 16.a. Let done be false. bool done = false; // 16.b. Repeat, while done is false, @@ -1515,17 +1658,17 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec // 16.b.iii. Assert: requiredModule is a Cyclic Module Record. auto* cyclic = uncheckedDowncast(requiredModule); // cyclic is a downcasted alias of requiredModule. // 16.b.iv. Assert: requiredModule.[[AsyncEvaluationOrder]] is either an integer or UNSET. - ASSERT(cyclic->asyncEvaluationOrder().hasOrder() || cyclic->asyncEvaluationOrder().isUnset()); + ASSERT(cyclic->asyncEvaluationOrder(instance).hasOrder() || cyclic->asyncEvaluationOrder(instance).isUnset()); // 16.b.v. If requiredModule.[[AsyncEvaluationOrder]] is UNSET, set requiredModule.[[Status]] to EVALUATED. - if (cyclic->asyncEvaluationOrder().isUnset()) { - cyclic->setStatus(Status::Evaluated); + if (cyclic->asyncEvaluationOrder(instance).isUnset()) { + cyclic->setStatus(instance, Status::Evaluated); // 16.b.vi. Otherwise, set requiredModule.[[Status]] to EVALUATING-ASYNC. } else - cyclic->setStatus(Status::EvaluatingAsync); + cyclic->setStatus(instance, Status::EvaluatingAsync); // 16.b.vii. If requiredModule and module are the same Module Record, set done to true. done = requiredModule == module; // 16.b.viii. Set requiredModule.[[CycleRoot]] to module. - requiredModule->setCycleRoot(vm, module); + cyclic->setCycleRoot(vm, instance, module); } while (!done); } // 17. Return index. diff --git a/Source/JavaScriptCore/runtime/AbstractModuleRecord.h b/Source/JavaScriptCore/runtime/AbstractModuleRecord.h index ed790d721a56..da53c5bde0af 100644 --- a/Source/JavaScriptCore/runtime/AbstractModuleRecord.h +++ b/Source/JavaScriptCore/runtime/AbstractModuleRecord.h @@ -40,6 +40,7 @@ namespace JSC { class CyclicModuleRecord; class JSModuleEnvironment; class JSModuleNamespaceObject; +class ModuleGraphInstance; class JSMap; class JSPromise; @@ -197,6 +198,23 @@ class AbstractModuleRecord : public JSInternalFieldObjectImpl<2> { const Identifier& moduleKey() const { return m_moduleKey; } ScriptFetchParameters::Type moduleType() const; const Vector& requestedModules() const LIFETIME_BOUND { return m_requestedModules; } + + // Import slots (module graph instances): the distinct records this module's + // named imports resolve to, fixed at link time. Every JSModuleEnvironment of + // this record carries one trailing slot per entry holding the environment of + // that exporter in the same graph instance, so a ModuleVar access is + // "walk to the importing environment, load slot" in every tier. + unsigned importSlotCount() const { return m_importedRecords.size(); } + AbstractModuleRecord* importedRecordAt(unsigned index) const { return m_importedRecords[index].get(); } + std::optional importSlotIndexFor(AbstractModuleRecord* exporter) const + { + for (unsigned i = 0; i < m_importedRecords.size(); ++i) { + if (m_importedRecords[i].get() == exporter) + return i; + } + return std::nullopt; + } + void setImportedRecords(VM&, const Vector&); ModuleMap& loadedModules() LIFETIME_BOUND { return m_loadedModules; } const ModuleMap& loadedModules() const LIFETIME_BOUND { return m_loadedModules; } const ExportEntries& exportEntries() const LIFETIME_BOUND { return m_exportEntries; } @@ -242,6 +260,10 @@ class AbstractModuleRecord : public JSInternalFieldObjectImpl<2> { void setImportedModule(JSGlobalObject*, const ModuleRequest&, AbstractModuleRecord*); JSModuleNamespaceObject* getModuleNamespace(JSGlobalObject*, ModulePhase = ModulePhase::Evaluation, bool shouldPreventExtensions = true); + // Module graph instances (prototype): a namespace object bound to `instance`'s environments (not cached on the record). + JS_EXPORT_PRIVATE JSModuleNamespaceObject* getModuleNamespace(JSGlobalObject*, ModuleGraphInstance*, ModulePhase = ModulePhase::Evaluation); + // This record's environment in `instance` (creating it for synthetic records with per-graph state when asked), or null. + JS_EXPORT_PRIVATE JSModuleEnvironment* graphInstanceEnvironment(JSGlobalObject*, ModuleGraphInstance*, bool createForSynthetic); #if USE(BUN_JSC_ADDITIONS) JSModuleNamespaceObject* getModuleNamespace(JSGlobalObject* globalObject, bool shouldPreventExtensions) { @@ -249,9 +271,9 @@ class AbstractModuleRecord : public JSInternalFieldObjectImpl<2> { } #endif - void gatherAsynchronousTransitiveDependencies(OrderedHashSet& result, UncheckedKeyHashSet& seen); - bool readyForSyncExecution(); - void evaluateSync(JSGlobalObject*); + void gatherAsynchronousTransitiveDependencies(OrderedHashSet& result, UncheckedKeyHashSet& seen, ModuleGraphInstance* = nullptr); + bool readyForSyncExecution(ModuleGraphInstance* = nullptr); + void evaluateSync(JSGlobalObject*, ModuleGraphInstance* = nullptr); JSPromise* asyncCapability() const; void asyncCapability(VM&, JSPromise*); @@ -274,9 +296,9 @@ class AbstractModuleRecord : public JSInternalFieldObjectImpl<2> { void evaluateModuleSync(JSGlobalObject*); #if USE(BUN_JSC_ADDITIONS) - unsigned innerModuleEvaluation(JSGlobalObject*, Vector& stack, unsigned index, int64_t referrerAsyncOrder, JSPromise* dynamicImportPromise); + unsigned innerModuleEvaluation(JSGlobalObject*, Vector& stack, unsigned index, int64_t referrerAsyncOrder, JSPromise* dynamicImportPromise, ModuleGraphInstance*); #else - unsigned innerModuleEvaluation(JSGlobalObject*, Vector& stack, unsigned index); + unsigned innerModuleEvaluation(JSGlobalObject*, Vector& stack, unsigned index, ModuleGraphInstance*); #endif unsigned innerModuleLinking(JSGlobalObject*, Vector& stack, unsigned index, RefPtr); @@ -348,6 +370,8 @@ class AbstractModuleRecord : public JSInternalFieldObjectImpl<2> { std::optional m_pendingAsyncDependencies; bool m_hasTLA { false }; + Vector> m_importedRecords; + bool m_importedRecordsSet { false }; SourceProviderSourceType m_sourceType; }; diff --git a/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp b/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp index 3557e2387c8f..8d5142080910 100644 --- a/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp +++ b/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp @@ -47,6 +47,7 @@ #include "JSCellButterfly.h" #include "JSIteratorHelper.h" #include "JSLexicalEnvironment.h" +#include "JSModuleEnvironment.h" #include "JSMap.h" #include "JSMapIterator.h" #include "JSPromise.h" @@ -1390,15 +1391,19 @@ JSC_DEFINE_COMMON_SLOW_PATH(slow_path_resolve_scope) auto& metadata = bytecode.metadata(codeBlock); const Identifier& ident = codeBlock->identifier(bytecode.m_var); JSScope* scope = callFrame->uncheckedR(bytecode.m_scope).Register::scope(); + + if (metadata.m_resolveType == ModuleVar) { + JSObject* result = JSModuleEnvironment::resolveModuleVarScope(globalObject, scope, metadata.m_localScopeDepth, uncheckedDowncast(metadata.m_lexicalEnvironment.get())); + CHECK_EXCEPTION(); + RETURN(result); + } + JSObject* resolvedScope = JSScope::resolve(globalObject, scope, ident); // Proxy can throw an error here, e.g. Proxy in with statement's @unscopables. CHECK_EXCEPTION(); ResolveType resolveType = metadata.m_resolveType; - // ModuleVar does not keep the scope register value alive in DFG. - ASSERT(resolveType != ModuleVar); - switch (resolveType) { case GlobalProperty: case GlobalPropertyWithVarInjectionChecks: diff --git a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp index 1fe1a1ba1059..4a4cd35795cb 100644 --- a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp @@ -36,6 +36,8 @@ #include "JSModuleLoader.h" #include "JSModuleNamespaceObject.h" #include "JSModuleRecord.h" +#include "ModuleGraphInstance.h" +#include "Options.h" #include "JSPromise.h" #include "ModuleProgramExecutable.h" #include "SourceProfiler.h" @@ -50,6 +52,144 @@ namespace JSC { +// Evaluation state of this record in a module graph instance. A record the +// instance did not instantiate (shared with the primary graph) uses its own. +static ModuleRecordInstance* recordInstanceFor(const CyclicModuleRecord* record, ModuleGraphInstance* instance) +{ + if (!instance) + return nullptr; + // Evaluating an instance never reads or writes the primary graph's state: + // instantiation gives every Cyclic Module Record the instance reaches its + // own state (JSModuleRecord::createInstanceEnvironment), and a cleared + // instance is refused before evaluation starts. + ModuleRecordInstance* recordInstance = instance->recordInstance(const_cast(record)); + RELEASE_ASSERT(recordInstance, "module graph instance has no state for a cyclic module record it evaluates"); + return recordInstance; +} + +CyclicModuleRecord::Status CyclicModuleRecord::status(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->status(); + return status(); +} + +JSValue CyclicModuleRecord::evaluationError(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->evaluationError(); + return evaluationError(); +} + +unsigned CyclicModuleRecord::dfsAncestorIndex(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->dfsAncestorIndex(); + return dfsAncestorIndex(); +} + +CyclicModuleRecord* CyclicModuleRecord::cycleRoot(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->cycleRoot(); + return AbstractModuleRecord::cycleRoot(); +} + +AbstractModuleRecord::AsyncEvaluationOrder CyclicModuleRecord::asyncEvaluationOrder(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->asyncEvaluationOrder(); + return AbstractModuleRecord::asyncEvaluationOrder(); +} + +std::optional CyclicModuleRecord::pendingAsyncDependencies(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->pendingAsyncDependencies(); + return AbstractModuleRecord::pendingAsyncDependencies(); +} + +JSPromise* CyclicModuleRecord::topLevelCapability(ModuleGraphInstance* instance) const +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + return state->topLevelCapability(); + return AbstractModuleRecord::topLevelCapability(); +} + +void CyclicModuleRecord::setStatus(ModuleGraphInstance* instance, Status newStatus) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setStatus(newStatus); + else + setStatus(newStatus); +} + +void CyclicModuleRecord::setEvaluationError(VM& vm, ModuleGraphInstance* instance, JSValue error) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setEvaluationError(vm, error); + else + setEvaluationError(vm, error); +} + +void CyclicModuleRecord::setDFSAncestorIndex(ModuleGraphInstance* instance, unsigned index) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setDFSAncestorIndex(index); + else + setDFSAncestorIndex(index); +} + +void CyclicModuleRecord::setCycleRoot(VM& vm, ModuleGraphInstance* instance, CyclicModuleRecord* root) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setCycleRoot(vm, root); + else + AbstractModuleRecord::setCycleRoot(vm, root); +} + +void CyclicModuleRecord::setAsyncEvaluationOrder(ModuleGraphInstance* instance, AsyncEvaluationOrder order) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setAsyncEvaluationOrder(order); + else + AbstractModuleRecord::setAsyncEvaluationOrder(order); +} + +void CyclicModuleRecord::setPendingAsyncDependencies(ModuleGraphInstance* instance, std::optional value) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setPendingAsyncDependencies(value); + else + AbstractModuleRecord::setPendingAsyncDependencies(value); +} + +void CyclicModuleRecord::appendAsyncParentModule(VM& vm, ModuleGraphInstance* instance, AbstractModuleRecord* parent) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->appendAsyncParentModule(vm, parent); + else + AbstractModuleRecord::appendAsyncParentModule(vm, parent); +} + +void CyclicModuleRecord::setTopLevelCapability(VM& vm, ModuleGraphInstance* instance, JSPromise* capability) +{ + if (ModuleRecordInstance* state = recordInstanceFor(this, instance)) + state->setTopLevelCapability(vm, capability); + else + AbstractModuleRecord::setTopLevelCapability(vm, capability); +} + +template +void CyclicModuleRecord::forEachAsyncParentModule(ModuleGraphInstance* instance, const Functor& functor) const +{ + ModuleRecordInstance* state = recordInstanceFor(this, instance); + const Vector>& parents = state ? state->asyncParentModules() : asyncParentModules(); + for (const WriteBarrier& parent : parents) + functor(uncheckedDowncast(parent.get())); +} + + const ClassInfo CyclicModuleRecord::s_info = { "CyclicModuleRecord"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(CyclicModuleRecord) }; CyclicModuleRecord::CyclicModuleRecord(VM& vm, Structure* structure, const Identifier& moduleKey, SourceProviderSourceType sourceType) @@ -149,11 +289,37 @@ void CyclicModuleRecord::initializeEnvironment(JSGlobalObject* globalObject, Ref // 4. Assert: realm is not undefined. SymbolTable* symbolTable = nullptr; if (jsModule) { + // Module graph instances: fix the import slots (distinct exporter records + // of the named imports) before the environment is sized. + if (Options::useModuleGraphInstances()) { + Vector importedRecords; + UncheckedKeyHashSet seenRecords; + for (const auto& [key, in] : importEntries()) { +#if USE(BUN_JSC_ADDITIONS) + // SingleTypeScript: a named import that may be absent; when it + // does resolve it is bound (and needs a slot) exactly like Single. + if (in.type != ImportEntryType::Single && in.type != ImportEntryType::SingleTypeScript) + continue; +#else + if (in.type != ImportEntryType::Single) + continue; +#endif + AbstractModuleRecord* importedModule = hostResolveImportedModule(globalObject, in.moduleRequest, in.moduleRequestType); + RETURN_IF_EXCEPTION(scope, void()); + Resolution resolution = importedModule->resolveExport(globalObject, in.importName); + RETURN_IF_EXCEPTION(scope, void()); + if (resolution.type != Resolution::Type::Resolved) + continue; + if (seenRecords.add(resolution.moduleRecord).isNewEntry) + importedRecords.append(resolution.moduleRecord); + } + setImportedRecords(vm, importedRecords); + } // 5. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]). moduleProgramExecutable = jsModule->getOrMakeExecutable(globalObject); RETURN_IF_EXCEPTION(scope, void()); symbolTable = moduleProgramExecutable->moduleEnvironmentSymbolTable(); - env = JSModuleEnvironment::create(vm, globalObject, globalObject->globalLexicalEnvironment(), symbolTable, jsTDZValue(), this); + env = JSModuleEnvironment::create(vm, globalObject, globalObject->moduleEnvironmentParentScope(), symbolTable, jsTDZValue(), this); RETURN_IF_EXCEPTION(scope, void()); // 6. Set module.[[Environment]] to env. setModuleEnvironment(globalObject, env); @@ -331,6 +497,9 @@ void CyclicModuleRecord::initializeEnvironment(JSGlobalObject* globalObject, Ref // 22. Let lexDeclarations be the LexicallyScopedDeclarations of code. // 23. Let privateEnv be null. // 24. For each element d of lexDeclarations, do + Vector> functionDeclExecutables; + if (Options::useModuleGraphInstances()) + functionDeclExecutables.grow(unlinkedCodeBlock->numberOfFunctionDecls()); for (size_t i = 0, numberOfFunctions = unlinkedCodeBlock->numberOfFunctionDecls(); i < numberOfFunctions; ++i) { // 24.a. For each element dn of the BoundNames of d, do // 24.a.i. If IsConstantDeclaration of d is true, then @@ -351,6 +520,8 @@ void CyclicModuleRecord::initializeEnvironment(JSGlobalObject* globalObject, Ref // 24.a.iii.1. Let fo be InstantiateFunctionObject of d with arguments env and privateEnv. auto* executable = unlinkedFunctionExecutable->link(vm, moduleProgramExecutable, moduleProgramExecutable->source()); RETURN_IF_EXCEPTION(scope, void()); + if (Options::useModuleGraphInstances()) + functionDeclExecutables[i].setWithoutWriteBarrier(executable); SourceParseMode parseMode = executable->parseMode(); JSFunction* function = nullptr; if (isAsyncGeneratorWrapperParseMode(parseMode)) @@ -369,6 +540,9 @@ void CyclicModuleRecord::initializeEnvironment(JSGlobalObject* globalObject, Ref } } + if (Options::useModuleGraphInstances()) + jsModule->retainForGraphInstances(vm, moduleProgramExecutable, WTF::move(functionDeclExecutables)); + if (jsModule->features() & ImportMetaFeature) { JSObject* metaProperties = globalObject->moduleLoader()->createImportMetaProperties(globalObject, identifierToJSValue(vm, moduleKey()), jsModule, scriptFetcher); RETURN_IF_EXCEPTION(scope, void()); @@ -419,36 +593,40 @@ void CyclicModuleRecord::link(JSGlobalObject* globalObject, RefPtrvm(); auto scope = DECLARE_THROW_SCOPE(vm); + if (instance && instance->isCleared()) [[unlikely]] + RELEASE_AND_RETURN(scope, JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Module graph instance was disposed"_s))); + // 1. Assert: This call to Evaluate is not happening at the same time as another call to Evaluate within the surrounding agent. // FIXME: is this needed? // 2. Assert: module.[[Status]] is one of LINKED, EVALUATING-ASYNC, or EVALUATED. - ASSERT(m_status == Status::Linked || m_status == Status::EvaluatingAsync || m_status == Status::Evaluated); + ASSERT(status(instance) == Status::Linked || status(instance) == Status::EvaluatingAsync || status(instance) == Status::Evaluated); CyclicModuleRecord* module = this; // 3. If module.[[Status]] is either EVALUATING-ASYNC or EVALUATED, then - if (m_status == Status::EvaluatingAsync || m_status == Status::Evaluated) { + if (status(instance) == Status::EvaluatingAsync || status(instance) == Status::Evaluated) { // 3.a. If module.[[CycleRoot]] is not EMPTY, then - if (CyclicModuleRecord* root = m_cycleRoot.get()) { + if (CyclicModuleRecord* root = cycleRoot(instance)) { // 3.a.i. Set module to module.[[CycleRoot]]. module = root; // 3.b. Else, } else { // 3.b.i. Assert: module.[[Status]] is EVALUATED and module.[[EvaluationError]] is a throw completion. - ASSERT(m_status == Status::Evaluated); - ASSERT(m_evaluationError); + ASSERT(status(instance) == Status::Evaluated); + ASSERT(evaluationError(instance)); } } // 4. If module.[[TopLevelCapability]] is not EMPTY, then - if (JSPromise* promise = module->topLevelCapability()) { + if (JSPromise* promise = module->topLevelCapability(instance)) { // 4.a. Return module.[[TopLevelCapability]].[[Promise]]. RELEASE_AND_RETURN(scope, promise); } @@ -457,12 +635,12 @@ JSPromise* CyclicModuleRecord::evaluate(JSGlobalObject* globalObject) // 6. Let capability be ! NewPromiseCapability(%Promise%). JSPromise* capability = JSPromise::create(vm, globalObject->promiseStructure()); // 7. Set module.[[TopLevelCapability]] to capability. - module->setTopLevelCapability(vm, capability); + module->setTopLevelCapability(vm, instance, capability); // 8. Let result be Completion(InnerModuleEvaluation(module, stack, 0)). #if USE(BUN_JSC_ADDITIONS) - module->innerModuleEvaluation(globalObject, stack, 0, referrerAsyncOrder, dynamicImportPromise); + module->innerModuleEvaluation(globalObject, stack, 0, referrerAsyncOrder, dynamicImportPromise, instance); #else - module->innerModuleEvaluation(globalObject, stack, 0); + module->innerModuleEvaluation(globalObject, stack, 0, instance); #endif // 9. If result is an abrupt completion, then if (Exception* exception = scope.exception()) { @@ -471,28 +649,28 @@ JSPromise* CyclicModuleRecord::evaluate(JSGlobalObject* globalObject) for (AbstractModuleRecord* abstractRecord : stack) { // 9.a.i. Assert: m.[[Status]] is EVALUATING. auto* cyclic = uncheckedDowncast(abstractRecord); - ASSERT(cyclic->status() == Status::Evaluating); + ASSERT(cyclic->status(instance) == Status::Evaluating); // 9.a.ii. Set m.[[Status]] to EVALUATED. - cyclic->setStatus(Status::Evaluated); + cyclic->setStatus(instance, Status::Evaluated); // 9.a.iii. Set m.[[EvaluationError]] to result. - cyclic->setEvaluationError(vm, exception->value()); + cyclic->setEvaluationError(vm, instance, exception->value()); } // 9.b. Assert: module.[[Status]] is EVALUATED. - ASSERT(module->status() == Status::Evaluated); + ASSERT(module->status(instance) == Status::Evaluated); // 9.c. Assert: module.[[EvaluationError]] and result are the same Completion Record. - ASSERT(module->evaluationError() == exception->value()); + ASSERT(module->evaluationError(instance) == exception->value()); // 9.d. Perform ! Call(capability.[[Reject]], undefined, « result.[[Value]] »). capability->rejectWithCaughtException(vm, scope); // 10. Else, } else { // 10.a. Assert: module.[[Status]] is either EVALUATING-ASYNC or EVALUATED. - ASSERT(module->status() == Status::EvaluatingAsync || module->status() == Status::Evaluated); + ASSERT(module->status(instance) == Status::EvaluatingAsync || module->status(instance) == Status::Evaluated); // 10.b. Assert: module.[[EvaluationError]] is EMPTY. - ASSERT(module->evaluationError() == nullptr); + ASSERT(!module->evaluationError(instance)); // 10.c. If module.[[Status]] is EVALUATED, then - if (module->status() == Status::Evaluated) { + if (module->status(instance) == Status::Evaluated) { // 10.c.i. Assert: module.[[AsyncEvaluationOrder]] is either UNSET or DONE. - ASSERT(module->asyncEvaluationOrder().isUnset() || module->asyncEvaluationOrder().isDone()); + ASSERT(module->asyncEvaluationOrder(instance).isUnset() || module->asyncEvaluationOrder(instance).isDone()); // 10.c.ii. NOTE: module.[[AsyncEvaluationOrder]] is DONE if and only if module had already been evaluated and that evaluation was asynchronous. // 10.c.iii. Perform ! Call(capability.[[Resolve]], undefined, « undefined »). capability->fulfill(vm, jsUndefined()); @@ -504,11 +682,14 @@ JSPromise* CyclicModuleRecord::evaluate(JSGlobalObject* globalObject) RELEASE_AND_RETURN(scope, capability); } -void CyclicModuleRecord::execute(JSGlobalObject* globalObject, JSPromise* capability) +void CyclicModuleRecord::execute(JSGlobalObject* globalObject, JSPromise* capability, ModuleGraphInstance* instance) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + if (ModuleRecordInstance* recordInstance = recordInstanceFor(this, instance)) + RELEASE_AND_RETURN(scope, uncheckedDowncast(this)->executeInstance(globalObject, recordInstance, capability)); + #if ENABLE(WEBASSEMBLY) if (auto* wasmModule = dynamicDowncast(this)) { wasmModule->initializeImports(globalObject, nullptr, Wasm::CreationMode::FromModuleLoader); @@ -530,7 +711,7 @@ void CyclicModuleRecord::execute(JSGlobalObject* globalObject, JSPromise* capabi RELEASE_AND_RETURN(scope, uncheckedDowncast(this)->execute(globalObject, capability)); } -void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) +void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject, ModuleGraphInstance* instance) { // ExecuteAsyncModule(module) // https://tc39.es/ecma262/#sec-execute-async-module @@ -539,7 +720,7 @@ void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) auto scope = DECLARE_THROW_SCOPE(vm); // 1. Assert: module.[[Status]] is either EVALUATING or EVALUATING-ASYNC. - ASSERT(status() == Status::Evaluating || status() == Status::EvaluatingAsync); + ASSERT(status(instance) == Status::Evaluating || status(instance) == Status::EvaluatingAsync); // 2. Assert: module.[[HasTLA]] is true. ASSERT(hasTLA()); // 3. Let capability be ! NewPromiseCapability(%Promise%). @@ -555,15 +736,19 @@ void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) // 7. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »). // Also handled in JSMicrotask.cpp. // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this); + // The context names the module and, for a module graph instance, the instance (its ModuleRecordInstance). + JSCell* context = this; + if (ModuleRecordInstance* recordInstance = recordInstanceFor(this, instance)) + context = recordInstance; + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, context); // 9. Perform ! module.ExecuteModule(capability). - execute(globalObject, promise); + execute(globalObject, promise, instance); RETURN_IF_EXCEPTION(scope, void()); // 10. Return UNUSED. RELEASE_AND_RETURN(scope, void()); } -static void gatherAvailableAncestors(CyclicModuleRecord* module, Vector& execList) +static void gatherAvailableAncestors(CyclicModuleRecord* module, Vector& execList, ModuleGraphInstance* instance) { // GatherAvailableAncestors(module, execList) // https://tc39.es/ecma262/#sec-gather-available-ancestors @@ -581,30 +766,29 @@ static void gatherAvailableAncestors(CyclicModuleRecord* module, Vector& barrier : record->asyncParentModules()) { - auto* m = uncheckedDowncast(barrier.get()); + record->forEachAsyncParentModule(instance, [&](CyclicModuleRecord* m) { // 1.a. If execList does not contain m and m.[[CycleRoot]].[[EvaluationError]] is empty, then // (Probable spec bug (https://github.com/tc39/ecma262/issues/3766). We need an additional check here that m.[[CycleRoot]] isn't empty.) - ASSERT_IMPLIES(!m->cycleRoot(), m->evaluationError()); - CyclicModuleRecord* root = m->cycleRoot(); - if (!root || root->evaluationError() != nullptr) - continue; - auto pending = m->pendingAsyncDependencies(); + ASSERT_IMPLIES(!m->cycleRoot(instance), m->evaluationError(instance)); + CyclicModuleRecord* root = m->cycleRoot(instance); + if (!root || !!root->evaluationError(instance)) + return; + auto pending = m->pendingAsyncDependencies(instance); // Verify the invariant in debug: execList ∋ m iff pending == 0 (under cycleRoot OK). ASSERT(pending); ASSERT(execList.contains(m) == !*pending); if (!*pending) - continue; + return; // 1.a.i. Assert: m.[[Status]] is EVALUATING-ASYNC. - ASSERT(m->status() == CyclicModuleRecord::Status::EvaluatingAsync); + ASSERT(m->status(instance) == CyclicModuleRecord::Status::EvaluatingAsync); // 1.a.ii. Assert: m.[[EvaluationError]] is EMPTY. - ASSERT(m->evaluationError() == nullptr); + ASSERT(!m->evaluationError(instance)); // 1.a.iii. Assert: m.[[AsyncEvaluationOrder]] is an integer. - ASSERT(m->asyncEvaluationOrder().hasOrder()); + ASSERT(m->asyncEvaluationOrder(instance).hasOrder()); // 1.a.iv. Assert: m.[[PendingAsyncDependencies]] > 0. (Implied by *pending != 0 above.) // 1.a.v. Set m.[[PendingAsyncDependencies]] to m.[[PendingAsyncDependencies]] - 1. int newDependencies = *pending - 1; - m->setPendingAsyncDependencies(newDependencies); + m->setPendingAsyncDependencies(instance, newDependencies); // 1.a.vi. If m.[[PendingAsyncDependencies]] = 0, then if (!newDependencies) { // 1.a.vi.1. Append m to execList. @@ -613,12 +797,12 @@ static void gatherAvailableAncestors(CyclicModuleRecord* module, VectorhasTLA()) worklist.append(m); } - } + }); } // 2. Return UNUSED. } -void CyclicModuleRecord::asyncExecutionRejected(JSGlobalObject* globalObject, JSValue error) +void CyclicModuleRecord::asyncExecutionRejected(JSGlobalObject* globalObject, JSValue error, ModuleGraphInstance* instance) { // AsyncModuleExecutionRejected(module, error) // https://tc39.es/ecma262/#sec-async-module-execution-rejected @@ -632,41 +816,45 @@ void CyclicModuleRecord::asyncExecutionRejected(JSGlobalObject* globalObject, JS while (!stack.isEmpty()) { CyclicModuleRecord* module = stack.takeLast(); // 1. If module.[[Status]] is EVALUATED, then - if (module->status() == CyclicModuleRecord::Status::Evaluated) { + if (module->status(instance) == CyclicModuleRecord::Status::Evaluated) { // 1.a. Assert: module.[[EvaluationError]] is not EMPTY. - ASSERT(module->evaluationError() != nullptr); + ASSERT(!!module->evaluationError(instance)); // 1.b. Return UNUSED. continue; } // 2. Assert: module.[[Status]] is EVALUATING-ASYNC. - ASSERT(module->status() == CyclicModuleRecord::Status::EvaluatingAsync); + ASSERT(module->status(instance) == CyclicModuleRecord::Status::EvaluatingAsync); // 3. Assert: module.[[AsyncEvaluationOrder]] is an integer. - ASSERT(module->asyncEvaluationOrder().hasOrder()); + ASSERT(module->asyncEvaluationOrder(instance).hasOrder()); // 4. Assert: module.[[EvaluationError]] is EMPTY. - ASSERT(module->evaluationError() == nullptr); + ASSERT(!module->evaluationError(instance)); // 5. Set module.[[EvaluationError]] to ThrowCompletion(error). - module->setEvaluationError(vm, error); + module->setEvaluationError(vm, instance, error); // 6. Set module.[[Status]] to EVALUATED. - module->setStatus(CyclicModuleRecord::Status::Evaluated); + module->setStatus(instance, CyclicModuleRecord::Status::Evaluated); // 7. Set module.[[AsyncEvaluationOrder]] to DONE. - module->setAsyncEvaluationOrder(AbstractModuleRecord::AsyncEvaluationOrder::done()); + module->setAsyncEvaluationOrder(instance, AbstractModuleRecord::AsyncEvaluationOrder::done()); // 8. NOTE: module.[[AsyncEvaluationOrder]] is set to DONE for symmetry with AsyncModuleExecutionFulfilled. In InnerModuleEvaluation, the value of a module's [[AsyncEvaluationOrder]] internal slot is unused when its [[EvaluationError]] internal slot is not EMPTY. // 9. If module.[[TopLevelCapability]] is not EMPTY, then - if (auto* topLevel = module->topLevelCapability()) { + if (auto* topLevel = module->topLevelCapability(instance)) { // 9.a. Assert: module.[[CycleRoot]] and module are the same Module Record. - ASSERT(module->cycleRoot() == module); + ASSERT(module->cycleRoot(instance) == module); // 9.b. Perform ! Call(module.[[TopLevelCapability]].[[Reject]], undefined, « error »). topLevel->reject(vm, error); } // 10. For each Cyclic Module Record m of module.[[AsyncParentModules]], do // 10.a. Perform AsyncModuleExecutionRejected(m, error). - for (const WriteBarrier& m : module->asyncParentModules() | std::views::reverse) - stack.append(uncheckedDowncast(m.get())); + { + Vector parents; + module->forEachAsyncParentModule(instance, [&](CyclicModuleRecord* m) { parents.append(m); }); + for (CyclicModuleRecord* m : parents | std::views::reverse) + stack.append(m); + } } // 11. Return UNUSED. } -void CyclicModuleRecord::asyncExecutionFulfilled(JSGlobalObject* globalObject) +void CyclicModuleRecord::asyncExecutionFulfilled(JSGlobalObject* globalObject, ModuleGraphInstance* instance) { // AsyncModuleExecutionFulfilled(module) // https://tc39.es/ecma262/#sec-async-module-execution-fulfilled @@ -675,26 +863,26 @@ void CyclicModuleRecord::asyncExecutionFulfilled(JSGlobalObject* globalObject) auto scope = DECLARE_THROW_SCOPE(vm); // 1. If module.[[Status]] is EVALUATED, then - if (status() == CyclicModuleRecord::Status::Evaluated) { + if (status(instance) == CyclicModuleRecord::Status::Evaluated) { // 1.a. Assert: module.[[EvaluationError]] is not EMPTY. - ASSERT(evaluationError() != nullptr); + ASSERT(!!evaluationError(instance)); // 1.b. Return UNUSED. RELEASE_AND_RETURN(scope, void()); } // 2. Assert: module.[[Status]] is EVALUATING-ASYNC. - ASSERT(status() == CyclicModuleRecord::Status::EvaluatingAsync); + ASSERT(status(instance) == CyclicModuleRecord::Status::EvaluatingAsync); // 3. Assert: module.[[AsyncEvaluationOrder]] is an integer. - ASSERT(asyncEvaluationOrder().hasOrder()); + ASSERT(asyncEvaluationOrder(instance).hasOrder()); // 4. Assert: module.[[EvaluationError]] is EMPTY. - ASSERT(evaluationError() == nullptr); + ASSERT(!evaluationError(instance)); // 5. Set module.[[AsyncEvaluationOrder]] to DONE. - setAsyncEvaluationOrder(AbstractModuleRecord::AsyncEvaluationOrder::done()); + setAsyncEvaluationOrder(instance, AbstractModuleRecord::AsyncEvaluationOrder::done()); // 6. Set module.[[Status]] to EVALUATED. - setStatus(CyclicModuleRecord::Status::Evaluated); + setStatus(instance, CyclicModuleRecord::Status::Evaluated); // 7. If module.[[TopLevelCapability]] is not EMPTY, then - if (auto* capability = topLevelCapability()) { + if (auto* capability = topLevelCapability(instance)) { // 7.a. Assert: module.[[CycleRoot]] and module are the same Module Record. - ASSERT(cycleRoot() == this); + ASSERT(cycleRoot(instance) == this); // 7.b. Perform ! Call(module.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). capability->fulfill(vm, jsUndefined()); } @@ -702,54 +890,54 @@ void CyclicModuleRecord::asyncExecutionFulfilled(JSGlobalObject* globalObject) // (Note: it's safe to use a Vector instead of a MarkedArgumentsBuffer here because all the contents are accessed through WriteBarriers starting at `this`.) Vector execList; // 9. Perform GatherAvailableAncestors(module, execList). - gatherAvailableAncestors(this, execList); + gatherAvailableAncestors(this, execList, instance); // 10. Assert: All elements of execList have their [[AsyncEvaluationOrder]] field set to an integer, [[PendingAsyncDependencies]] field set to 0, and [[EvaluationError]] field set to EMPTY. #if ASSERT_ENABLED for (CyclicModuleRecord* element : execList) { - ASSERT(element->asyncEvaluationOrder().hasOrder()); - ASSERT(element->pendingAsyncDependencies() && !*element->pendingAsyncDependencies()); - ASSERT(!element->evaluationError()); + ASSERT(element->asyncEvaluationOrder(instance).hasOrder()); + ASSERT(element->pendingAsyncDependencies(instance) && !*element->pendingAsyncDependencies(instance)); + ASSERT(!element->evaluationError(instance)); } #endif // 11. Let sortedExecList be a List whose elements are the elements of execList, sorted by their [[AsyncEvaluationOrder]] field in ascending order. - std::ranges::sort(execList, [](CyclicModuleRecord* left, CyclicModuleRecord* right) { - return left->asyncEvaluationOrder().order() < right->asyncEvaluationOrder().order(); + std::ranges::sort(execList, [instance](CyclicModuleRecord* left, CyclicModuleRecord* right) { + return left->asyncEvaluationOrder(instance).order() < right->asyncEvaluationOrder(instance).order(); }); // 12. For each Cyclic Module Record m of sortedExecList, do for (CyclicModuleRecord* m : execList) { // 12.a. If m.[[Status]] is EVALUATED, then - if (m->status() == CyclicModuleRecord::Status::Evaluated) { + if (m->status(instance) == CyclicModuleRecord::Status::Evaluated) { // 12.a.i. Assert: m.[[EvaluationError]] is not EMPTY. - ASSERT(m->evaluationError() != nullptr); + ASSERT(!!m->evaluationError(instance)); // 12.b. Else if m.[[HasTLA]] is true, then } else if (m->hasTLA()) { // 12.b.i. Perform ExecuteAsyncModule(m). - m->executeAsync(globalObject); + m->executeAsync(globalObject, instance); if (Exception* exception = scope.exception()) { JSValue error = exception->value(); TRY_CLEAR_EXCEPTION(scope, void()); - m->asyncExecutionRejected(globalObject, error); + m->asyncExecutionRejected(globalObject, error, instance); } // 12.c. Else, } else { // 12.c.i. Let result be m.ExecuteModule(). - m->execute(globalObject); + m->execute(globalObject, nullptr, instance); // 12.c.ii. If result is an abrupt completion, then if (Exception* exception = scope.exception()) { JSValue error = exception->value(); TRY_CLEAR_EXCEPTION(scope, void()); // 12.c.ii.1. Perform AsyncModuleExecutionRejected(m, result.[[Value]]). - m->asyncExecutionRejected(globalObject, error); + m->asyncExecutionRejected(globalObject, error, instance); // 12.c.iii. Else, } else { // 12.c.iii.1. Set m.[[AsyncEvaluationOrder]] to DONE. - m->setAsyncEvaluationOrder(AbstractModuleRecord::AsyncEvaluationOrder::done()); + m->setAsyncEvaluationOrder(instance, AbstractModuleRecord::AsyncEvaluationOrder::done()); // 12.c.iii.2. Set m.[[Status]] to EVALUATED. - m->setStatus(CyclicModuleRecord::Status::Evaluated); + m->setStatus(instance, CyclicModuleRecord::Status::Evaluated); // 12.c.iii.3. If m.[[TopLevelCapability]] is not EMPTY, then - if (auto* capability = m->topLevelCapability()) { + if (auto* capability = m->topLevelCapability(instance)) { // 12.c.iii.3.a. Assert: m.[[CycleRoot]] and m are the same Module Record. - ASSERT(m->cycleRoot() == m); + ASSERT(m->cycleRoot(instance) == m); // 12.c.iii.3.b. Perform ! Call(m.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). capability->fulfill(vm, jsUndefined()); } diff --git a/Source/JavaScriptCore/runtime/CyclicModuleRecord.h b/Source/JavaScriptCore/runtime/CyclicModuleRecord.h index 030efb9eaabb..1c2a6b74212a 100644 --- a/Source/JavaScriptCore/runtime/CyclicModuleRecord.h +++ b/Source/JavaScriptCore/runtime/CyclicModuleRecord.h @@ -29,6 +29,8 @@ namespace JSC { +class ModuleGraphInstance; + class ErrorInstance; class CyclicModuleRecord : public AbstractModuleRecord { @@ -57,33 +59,63 @@ class CyclicModuleRecord : public AbstractModuleRecord { void initializeEnvironment(JSGlobalObject*, RefPtr); void link(JSGlobalObject*, RefPtr); + // Every evaluation entry point takes the module graph instance being + // evaluated (null: the primary instantiation, whose state lives on the + // record itself). See ModuleGraphInstance. #if USE(BUN_JSC_ADDITIONS) - JSPromise* evaluate(JSGlobalObject*, int64_t referrerAsyncOrder = -1, JSPromise* dynamicImportPromise = nullptr); + JSPromise* evaluate(JSGlobalObject*, int64_t referrerAsyncOrder = -1, JSPromise* dynamicImportPromise = nullptr, ModuleGraphInstance* = nullptr); #else - JSPromise* evaluate(JSGlobalObject*); + JSPromise* evaluate(JSGlobalObject*, ModuleGraphInstance* = nullptr); #endif - void execute(JSGlobalObject*, JSPromise* = nullptr); - void executeAsync(JSGlobalObject*); - void asyncExecutionFulfilled(JSGlobalObject*); - void asyncExecutionRejected(JSGlobalObject*, JSValue); + void execute(JSGlobalObject*, JSPromise* = nullptr, ModuleGraphInstance* = nullptr); + void executeAsync(JSGlobalObject*, ModuleGraphInstance* = nullptr); + void asyncExecutionFulfilled(JSGlobalObject*, ModuleGraphInstance* = nullptr); + void asyncExecutionRejected(JSGlobalObject*, JSValue, ModuleGraphInstance* = nullptr); Status status() const { return m_status; } JSValue evaluationError() const { return m_evaluationError.get(); } unsigned dfsAncestorIndex() const { return m_dfsAncestorIndex; } + // Evaluation state of this record in `instance` (null, or a record the + // instance shares with the primary graph: the record's own state). + using AbstractModuleRecord::cycleRoot; + using AbstractModuleRecord::asyncEvaluationOrder; + using AbstractModuleRecord::pendingAsyncDependencies; + using AbstractModuleRecord::topLevelCapability; + using AbstractModuleRecord::setCycleRoot; + using AbstractModuleRecord::setAsyncEvaluationOrder; + using AbstractModuleRecord::setPendingAsyncDependencies; + using AbstractModuleRecord::appendAsyncParentModule; + using AbstractModuleRecord::setTopLevelCapability; + Status status(ModuleGraphInstance*) const; + JSValue evaluationError(ModuleGraphInstance*) const; + unsigned dfsAncestorIndex(ModuleGraphInstance*) const; + CyclicModuleRecord* cycleRoot(ModuleGraphInstance*) const; + AsyncEvaluationOrder asyncEvaluationOrder(ModuleGraphInstance*) const; + std::optional pendingAsyncDependencies(ModuleGraphInstance*) const; + JSPromise* topLevelCapability(ModuleGraphInstance*) const; + void setStatus(ModuleGraphInstance*, Status); + void setEvaluationError(VM&, ModuleGraphInstance*, JSValue); + void setDFSAncestorIndex(ModuleGraphInstance*, unsigned); + void setCycleRoot(VM&, ModuleGraphInstance*, CyclicModuleRecord*); + void setAsyncEvaluationOrder(ModuleGraphInstance*, AsyncEvaluationOrder); + void setPendingAsyncDependencies(ModuleGraphInstance*, std::optional); + void appendAsyncParentModule(VM&, ModuleGraphInstance*, AbstractModuleRecord*); + void setTopLevelCapability(VM&, ModuleGraphInstance*, JSPromise*); + template void forEachAsyncParentModule(ModuleGraphInstance*, const Functor&) const; // https://tc39.es/proposal-defer-import-eval/#sec-IsModuleSCCEvaluated // A module in an import cycle reaches EVALUATED once its own body has run, so only its cycle // root reaching EVALUATED tells you the whole cycle is done. - bool isSCCEvaluated() const + bool isSCCEvaluated(ModuleGraphInstance* instance = nullptr) const { // 1. If module.[[CycleRoot]] is not EMPTY, then // 1.a. If module.[[CycleRoot]].[[Status]] is EVALUATED, return true. // 1.b. Return false. - if (CyclicModuleRecord* root = cycleRoot()) - return root->status() == Status::Evaluated; + if (CyclicModuleRecord* root = cycleRoot(instance)) + return root->status(instance) == Status::Evaluated; // 2. If module.[[Status]] is EVALUATED, return true. // 3. Return false. - return status() == Status::Evaluated; + return status(instance) == Status::Evaluated; } void setStatus(Status newStatus) { m_status = newStatus; } diff --git a/Source/JavaScriptCore/runtime/FunctionConstructor.cpp b/Source/JavaScriptCore/runtime/FunctionConstructor.cpp index 4e289bae6609..9735d9e0d6c2 100644 --- a/Source/JavaScriptCore/runtime/FunctionConstructor.cpp +++ b/Source/JavaScriptCore/runtime/FunctionConstructor.cpp @@ -221,6 +221,8 @@ JSObject* constructFunctionSkippingEvalEnabledCheck(JSGlobalObject* globalObject JSObject* exception = nullptr; FunctionExecutable* function = FunctionExecutable::fromGlobalCode(functionName, globalObject, WTF::move(program), sourceOrigin, taintedOrigin, sourceURL, position, lexicallyScopedFeatures, exception, overrideLineNumber, functionConstructorParametersEndPosition, functionConstructionMode); + // Creating the error object for a parse failure runs host error-info hooks that may themselves throw. + RETURN_IF_EXCEPTION(scope, nullptr); if (!function) [[unlikely]] { ASSERT(exception); throwException(globalObject, scope, exception); diff --git a/Source/JavaScriptCore/runtime/GetPutInfo.h b/Source/JavaScriptCore/runtime/GetPutInfo.h index 8233aa1f0b6f..af0c44b06e3d 100644 --- a/Source/JavaScriptCore/runtime/GetPutInfo.h +++ b/Source/JavaScriptCore/runtime/GetPutInfo.h @@ -218,6 +218,9 @@ struct ResolveOp { InlineWatchpointSet* watchpointSet; uintptr_t operand; RefPtr importedName; + // ModuleVar: 1 + the import slot's ScopeOffset in the importing environment + // (0 = none; resolve to the linked exporter environment as a constant). + unsigned moduleImportSlot { 0 }; }; class GetPutInfo { diff --git a/Source/JavaScriptCore/runtime/JSAsyncGeneratorInlines.h b/Source/JavaScriptCore/runtime/JSAsyncGeneratorInlines.h index 3e83f0451baa..41caff3bc255 100644 --- a/Source/JavaScriptCore/runtime/JSAsyncGeneratorInlines.h +++ b/Source/JavaScriptCore/runtime/JSAsyncGeneratorInlines.h @@ -27,6 +27,7 @@ #include "JSAsyncFunctionGenerator.h" #include "JSAsyncGenerator.h" +#include "ModuleGraphInstance.h" #include "JSInternalFieldObjectImplInlines.h" #include "JSModuleRecord.h" #include "JSPromise.h" @@ -36,7 +37,7 @@ namespace JSC { ALWAYS_INLINE void JSAsyncGenerator::enqueue(VM& vm, JSValue value, int32_t mode, JSObject* settlementTarget) { - ASSERT(settlementTarget->inherits() || settlementTarget->inherits() || settlementTarget->inherits() || settlementTarget->inherits()); + ASSERT(settlementTarget->inherits() || settlementTarget->inherits() || settlementTarget->inherits() || settlementTarget->inherits() || settlementTarget->inherits()); if (isQueueEmpty()) [[likely]] { setResumeValue(vm, value); setResumeMode(mode); diff --git a/Source/JavaScriptCore/runtime/JSGlobalObject.cpp b/Source/JavaScriptCore/runtime/JSGlobalObject.cpp index a0097ce72112..4018f353b03b 100644 --- a/Source/JavaScriptCore/runtime/JSGlobalObject.cpp +++ b/Source/JavaScriptCore/runtime/JSGlobalObject.cpp @@ -180,6 +180,7 @@ #include "JSModuleLoaderInlines.h" #include "JSModuleNamespaceObjectInlines.h" #include "JSModuleRecord.h" +#include "ModuleGraphInstanceInlines.h" #include "JSModuleRecordInlines.h" #include "JSNativeStdFunctionInlines.h" #include "JSONObjectInlines.h" @@ -687,6 +688,49 @@ JSC_DEFINE_HOST_FUNCTION(disableSuperSampler, (JSGlobalObject*, CallFrame*)) return JSValue::encode(jsUndefined()); } + +void JSGlobalObject::setCurrentGraphInstanceForLoading(VM& vm, ModuleGraphInstance* instance) +{ + if (instance) + m_currentGraphInstanceForLoading.set(vm, this, instance); + else + m_currentGraphInstanceForLoading.clear(); +} + +ModuleGraphInstance* JSGlobalObject::graphInstanceForScope(JSScope* scope, JSScope** overlayOut) +{ + // The innermost module environment on the chain names its instance directly; + // an overlay (when configured) names it for non-module code scoped to the + // instance. Either is decisive: the first one found ends the walk. + SymbolTable* overlayTable = m_moduleScopeOverlaySymbolTable.get(); + if (overlayOut) + *overlayOut = nullptr; + for (; scope; scope = scope->next()) { + if (auto* moduleEnvironment = dynamicDowncast(scope)) { + ModuleGraphInstance* instance = moduleEnvironment->graphInstance(); + if (overlayOut) { + *overlayOut = nullptr; + for (JSScope* outer = scope->next(); overlayTable && outer; outer = outer->next()) { + auto* environment = dynamicDowncast(outer); + if (environment && environment->symbolTable() == overlayTable) { + *overlayOut = environment; + break; + } + } + } + return instance; + } + auto* environment = dynamicDowncast(scope); + if (!overlayTable || !environment || environment->symbolTable() != overlayTable) + continue; + JSValue instance = environment->variableAt(ScopeOffset(0)).get(); + if (overlayOut) + *overlayOut = environment; + return instance && instance.isCell() ? dynamicDowncast(instance.asCell()) : nullptr; + } + return nullptr; +} + } // namespace JSC #include "JSGlobalObject.lut.h" @@ -1371,6 +1415,10 @@ void JSGlobalObject::init(VM& vm) [] (const Initializer& init) { init.set(JSModuleRecord::createStructure(init.vm, init.owner, jsNull())); }); + m_moduleGraphInstanceStructure.initLater( + [] (const Initializer& init) { + init.set(ModuleGraphInstance::createStructure(init.vm, init.owner, jsNull())); + }); m_syntheticModuleRecordStructure.initLater( [] (const Initializer& init) { init.set(SyntheticModuleRecord::createStructure(init.vm, init.owner, jsNull())); @@ -3106,6 +3154,9 @@ void JSGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_regExpProtoSymbolReplace); thisObject->m_throwTypeErrorArgumentsCalleeGetterSetter.visit(visitor); thisObject->m_moduleLoader.visit(visitor); + visitor.append(thisObject->m_moduleScopeOverlaySymbolTable); + visitor.append(thisObject->m_currentGraphInstanceForLoading); + visitor.append(thisObject->m_primaryModuleScopeOverlay); visitor.append(thisObject->m_objectPrototype); visitor.append(thisObject->m_functionPrototype); @@ -3194,6 +3245,7 @@ void JSGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_regExpMatchesArrayWithIndicesStructure); visitor.append(thisObject->m_regExpMatchesIndicesArrayStructure); thisObject->m_moduleRecordStructure.visit(visitor); + thisObject->m_moduleGraphInstanceStructure.visit(visitor); thisObject->m_syntheticModuleRecordStructure.visit(visitor); thisObject->m_moduleNamespaceObjectStructure.visit(visitor); thisObject->m_proxyObjectStructure.visit(visitor); @@ -4034,4 +4086,116 @@ Inspector::JSGlobalObjectInspectorController& JSGlobalObject::inspectorControlle } #endif + +void JSGlobalObject::configureModuleScopeOverlay(const Vector& names) +{ + VM& vm = this->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + RELEASE_ASSERT(Options::useModuleGraphInstances()); + // Module CodeBlocks are linked against the scope chain shape this sets up + // and are shared by every instance, so it is configured once, before any + // module environment exists in this global object. + RELEASE_ASSERT(!m_moduleScopeOverlaySymbolTable, "module scope overlay configured twice"); + RELEASE_ASSERT(!m_hasCreatedModuleEnvironment, "module scope overlay configured after a module was linked"); + + // 1. Snapshot the global's current values for the overlaid names (getters + // may run script; nothing is published yet if one throws). + Vector overlaid; + MarkedArgumentBuffer values; + for (auto& name : names) { + if (name.isPrivateName()) + continue; + JSValue value = get(this, name); + RETURN_IF_EXCEPTION(scope, void()); + overlaid.append(name); + values.append(value); + } + if (values.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(this, scope); + return; + } + + // 2. The symbol table every overlay shares. Slot 0 holds the module graph + // instance an overlay belongs to (empty in the primary graph's), so code + // running under an overlay can be attributed to its instance from the + // scope chain alone. + SymbolTable* symbolTable = SymbolTable::create(vm); + symbolTable->setScopeType(SymbolTable::ScopeType::LexicalScope); + { + auto offset = symbolTable->takeNextScopeOffset(NoLockingNecessary); + ASSERT_UNUSED(offset, !offset.offset()); + SymbolTableEntry entry(VarOffset(ScopeOffset(0)), static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum)); + symbolTable->set(NoLockingNecessary, vm.propertyNames->builtinNames().moduleGraphInstancePrivateName().impl(), WTF::move(entry)); + } + Vector offsets; + for (auto& name : overlaid) { + auto offset = symbolTable->takeNextScopeOffset(NoLockingNecessary); + symbolTable->set(NoLockingNecessary, name.impl(), SymbolTableEntry(VarOffset(offset))); + offsets.append(offset); + } + + // 3. The primary graph's overlay carries the snapshot; publish both together. + JSLexicalEnvironment* primary = JSLexicalEnvironment::create(vm, this, globalLexicalEnvironment(), symbolTable, jsUndefined()); + for (unsigned i = 0; i < offsets.size(); ++i) + primary->variableAt(offsets[i]).set(vm, primary, values.at(i)); + m_moduleScopeOverlaySymbolTable.set(vm, this, symbolTable); + m_primaryModuleScopeOverlay.set(vm, this, primary); +} + +JSScope* JSGlobalObject::moduleEnvironmentParentScope() +{ + m_hasCreatedModuleEnvironment = true; + if (auto* overlay = m_primaryModuleScopeOverlay.get()) + return overlay; + return globalLexicalEnvironment(); +} + +bool JSGlobalObject::isModuleScopeOverlay(JSScope* scope) const +{ + auto* environment = dynamicDowncast(scope); + return environment && m_moduleScopeOverlaySymbolTable && environment->symbolTable() == m_moduleScopeOverlaySymbolTable.get() && environment->next() == m_globalLexicalEnvironment.get(); +} + +JSLexicalEnvironment* JSGlobalObject::createModuleScopeOverlay(JSObject* values, ModuleGraphInstance* instance) +{ + VM& vm = this->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + SymbolTable* symbolTable = m_moduleScopeOverlaySymbolTable.get(); + if (!symbolTable) { + throwTypeError(this, scope, "module scope overlay is not configured"_s); + return nullptr; + } + JSLexicalEnvironment* primary = m_primaryModuleScopeOverlay.get(); + ASSERT(primary); + JSLexicalEnvironment* overlay = JSLexicalEnvironment::create(vm, this, globalLexicalEnvironment(), symbolTable, jsUndefined()); + Vector> slots; + { + ConcurrentJSLocker locker(symbolTable->m_lock); + for (auto iter = symbolTable->begin(locker), end = symbolTable->end(locker); iter != end; ++iter) + slots.append({ Identifier::fromUid(vm, iter->key.get()), iter->value.scopeOffset() }); + } + for (auto& [name, offset] : slots) { + if (name.isPrivateName()) { + overlay->variableAt(offset).set(vm, overlay, instance ? JSValue(instance) : jsUndefined()); + continue; + } + JSValue value; + if (values) { + PropertySlot slot(values, PropertySlot::InternalMethodType::GetOwnProperty); + bool hasOwn = values->methodTable()->getOwnPropertySlot(values, this, name, slot); + RETURN_IF_EXCEPTION(scope, nullptr); + if (hasOwn) { + value = slot.getValue(this, name); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + // An own property of `values` wins even when undefined; only an absent + // name defaults to the primary's value. + if (!value) + value = primary->variableAt(offset).get(); + overlay->variableAt(offset).set(vm, overlay, value); + } + return overlay; +} + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSGlobalObject.h b/Source/JavaScriptCore/runtime/JSGlobalObject.h index 2cfbfffa98f7..d368657eb557 100644 --- a/Source/JavaScriptCore/runtime/JSGlobalObject.h +++ b/Source/JavaScriptCore/runtime/JSGlobalObject.h @@ -99,6 +99,7 @@ class JSIteratorConstructor; class JSIteratorHelperPrototype; class JSIteratorPrototype; class JSModuleLoader; +class ModuleGraphInstance; class JSModuleRecord; class JSPromise; class JSPromiseConstructor; @@ -352,6 +353,10 @@ class JSGlobalObject : public JSSegmentedVariableObject { LazyProperty m_throwTypeErrorArgumentsCalleeGetterSetter; LazyProperty m_moduleLoader; + WriteBarrier m_moduleScopeOverlaySymbolTable; + WriteBarrier m_currentGraphInstanceForLoading; + WriteBarrier m_primaryModuleScopeOverlay; + bool m_hasCreatedModuleEnvironment { false }; WriteBarrier m_objectPrototype; WriteBarrier m_functionPrototype; @@ -456,6 +461,7 @@ class JSGlobalObject : public JSSegmentedVariableObject { LazyProperty m_accessorPropertyDescriptorObjectStructure; LazyProperty m_promiseCapabilityObjectStructure; LazyProperty m_moduleRecordStructure; + LazyProperty m_moduleGraphInstanceStructure; LazyProperty m_syntheticModuleRecordStructure; LazyProperty m_moduleNamespaceObjectStructure; LazyProperty m_proxyObjectStructure; @@ -912,6 +918,43 @@ class JSGlobalObject : public JSSegmentedVariableObject { JSModuleLoader* moduleLoader() const LIFETIME_BOUND { return m_moduleLoader.get(this); } + // Module graph instances (prototype): an optional lexical environment placed + // between every module environment and the global lexical environment, so a + // fixed set of free identifiers (e.g. `process`) resolve as closure variables + // that each graph instance can give its own values. Configured once, before + // the first module is linked, with the identifier list; the primary overlay + // holds the global's own values. + JS_EXPORT_PRIVATE void configureModuleScopeOverlay(const Vector& names); + SymbolTable* moduleScopeOverlaySymbolTable() const { return m_moduleScopeOverlaySymbolTable.get(); } + // True for a scope created by createModuleScopeOverlay (or the primary overlay). + bool isModuleScopeOverlay(JSScope*) const; + JSLexicalEnvironment* primaryModuleScopeOverlay() const { return m_primaryModuleScopeOverlay.get(); } + // Parent scope for module environments: the primary overlay if configured, else the global lexical environment. + JS_EXPORT_PRIVATE JSScope* moduleEnvironmentParentScope(); + // A new overlay for a graph instance: same shape as the primary, values from `values` (own properties by name), defaulting to the primary's. + JS_EXPORT_PRIVATE JSLexicalEnvironment* createModuleScopeOverlay(JSObject* values, ModuleGraphInstance* = nullptr); + // The graph instance (and its overlay) that code with this scope chain runs in, or null (primary / not configured). + JS_EXPORT_PRIVATE ModuleGraphInstance* graphInstanceForScope(JSScope*, JSScope** overlayOut = nullptr); + // Module graph instances: while a graph loads/links modules (synchronously), + // the host's fetch hooks attribute host-side module objects they create + // (e.g. CommonJS modules behind an ESM import) to this instance. + ModuleGraphInstance* currentGraphInstanceForLoading() const { return m_currentGraphInstanceForLoading.get(); } + JS_EXPORT_PRIVATE void setCurrentGraphInstanceForLoading(VM&, ModuleGraphInstance*); + // Brackets a load performed on behalf of `instance` (host-provided + // synthetic modules created meanwhile belong to it); restores on exit. + class GraphInstanceLoadingScope { + public: + GraphInstanceLoadingScope(JSGlobalObject* globalObject, ModuleGraphInstance* instance) + : m_globalObject(globalObject), m_previous(globalObject->currentGraphInstanceForLoading()) + { + globalObject->setCurrentGraphInstanceForLoading(globalObject->vm(), instance); + } + ~GraphInstanceLoadingScope() { m_globalObject->setCurrentGraphInstanceForLoading(m_globalObject->vm(), m_previous); } + private: + JSGlobalObject* m_globalObject; + ModuleGraphInstance* m_previous; + }; + ObjectPrototype* objectPrototype() const LIFETIME_BOUND { return m_objectPrototype.get(); } FunctionPrototype* functionPrototype() const LIFETIME_BOUND { return m_functionPrototype.get(); } ArrayPrototype* arrayPrototype() const LIFETIME_BOUND { return m_arrayPrototype.get(); } @@ -1059,6 +1102,7 @@ class JSGlobalObject : public JSSegmentedVariableObject { Structure* regExpStringIteratorStructure() const { return m_regExpStringIteratorStructure.get(); } Structure* remoteFunctionStructure() const { return m_remoteFunctionStructure.get(this); } Structure* moduleRecordStructure() const { return m_moduleRecordStructure.get(this); } + Structure* moduleGraphInstanceStructure() const { return m_moduleGraphInstanceStructure.get(this); } Structure* syntheticModuleRecordStructure() const { return m_syntheticModuleRecordStructure.get(this); } Structure* moduleNamespaceObjectStructure() const { return m_moduleNamespaceObjectStructure.get(this); } Structure* proxyObjectStructure() const { return m_proxyObjectStructure.get(this); } diff --git a/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp b/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp index 0e7ec7a72c0d..a047321b858f 100644 --- a/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp +++ b/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp @@ -34,6 +34,8 @@ #include "IntlDateTimeFormat.h" #include "JSCInlines.h" #include "JSModuleLoader.h" +#include "ModuleGraphInstance.h" +#include "JSModuleEnvironment.h" #include "JSPromise.h" #include "JSSet.h" #include "Lexer.h" @@ -819,6 +821,25 @@ JSC_DEFINE_HOST_FUNCTION(globalFuncImportModule, (JSGlobalObject* globalObject, // we should retrieve this from the arguments. JSValue parameters = callFrame->argument(1); bool deferred = callFrame->argument(2).isTrue(); + +#if USE(BUN_JSC_ADDITIONS) + // Module graph instances: import() from code that belongs to an instance + // loads the requested graph as a template (no evaluation of the primary) + // and instantiates it into the caller's instance. + if (Options::useModuleGraphInstances()) [[unlikely]] { + if (ModuleGraphInstance* instance = globalObject->graphInstanceForScope(callFrame->callerScope(vm))) { + JSPromise* promise = JSModuleLoader::importIntoGraphInstance(globalObject, specifier, parameters, sourceOrigin, instance, deferred); + if (scope.exception()) [[unlikely]] + return rejectWithCaughtException(); + return JSValue::encode(promise); + } + } + // A primary-graph import() issued by host code that runs during an + // instance's load is the primary's, not that instance's. + std::optional primaryLoading; + if (Options::useModuleGraphInstances() && globalObject->currentGraphInstanceForLoading()) [[unlikely]] + primaryLoading.emplace(globalObject, nullptr); +#endif auto* importPromise = globalObject->moduleLoader()->importModule(globalObject, specifier, parameters, sourceOrigin, deferred); if (scope.exception()) [[unlikely]] return rejectWithCaughtException(); diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index e6f506445856..1379eeb209e2 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -49,6 +49,7 @@ #include "JSModuleLoader.h" #include "JSModuleNamespaceObject.h" #include "JSModuleRecord.h" +#include "ModuleGraphInstance.h" #include "JSPromise.h" #include "JSPromiseCombinatorsGlobalContext.h" #include "JSPromiseConstructor.h" @@ -950,15 +951,16 @@ static void promiseFinallyReactionJob(JSGlobalObject* globalObject, VM& vm, JSPr promiseResolveThenableJob(globalObject, resolutionObject, then, resolve, reject, microtaskCallCache); } -static void asyncModuleExecutionDone(JSGlobalObject* globalObject, JSModuleRecord* module, JSValue value, JSPromise::Status status) +static void asyncModuleExecutionDone(JSGlobalObject* globalObject, JSModuleRecord* module, JSValue value, JSPromise::Status status, ModuleGraphInstance* instance = nullptr) { + ModuleGraphInstance::BusyScope busy(globalObject, instance); if (status == JSPromise::Status::Fulfilled) { - module->asyncExecutionFulfilled(globalObject); + module->asyncExecutionFulfilled(globalObject, instance); return; } ASSERT(status == JSPromise::Status::Rejected); - module->asyncExecutionRejected(globalObject, value); + module->asyncExecutionRejected(globalObject, value, instance); } void asyncModuleResolveEvaluation(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, JSModuleRecord* module, JSValue result) @@ -982,6 +984,24 @@ void asyncModuleResolveEvaluation(JSGlobalObject* globalObject, VM& vm, ThrowSco JSPromise::resolveWithInternalMicrotaskForAsyncAwait(globalObject, vm, result, InternalMicrotask::AsyncModuleExecutionResume, module); } +void asyncModuleResolveEvaluation(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, ModuleRecordInstance* recordInstance, JSValue result) +{ + auto* capability = recordInstance->asyncCapability(); + + if (scope.exception()) [[unlikely]] { + capability->rejectWithCaughtException(vm, scope); + return; + } + + if (result == vm.fastAsyncGeneratorSentinel()) + return; + + if (recordInstance->isTopLevelExecutionFinished()) + capability->resolve(globalObject, vm, result); + else + JSPromise::resolveWithInternalMicrotaskForAsyncAwait(globalObject, vm, result, InternalMicrotask::AsyncModuleExecutionResume, recordInstance); +} + static void asyncModuleExecutionResume(JSGlobalObject* globalObject, VM& vm, JSModuleRecord* module, JSValue resolution, JSPromise::Status status) { auto scope = DECLARE_THROW_SCOPE(vm); @@ -994,6 +1014,23 @@ static void asyncModuleExecutionResume(JSGlobalObject* globalObject, VM& vm, JSM asyncModuleResolveEvaluation(globalObject, vm, scope, module, result); } +static void asyncModuleExecutionResume(JSGlobalObject* globalObject, VM& vm, ModuleRecordInstance* recordInstance, JSValue resolution, JSPromise::Status status) +{ + if (recordInstance->graphInstance()->isCleared()) + return; // the instance was disposed while this module was suspended + ModuleGraphInstance::BusyScope busy(globalObject, recordInstance->graphInstance()); + + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue resumeMode = jsNumber(status == JSPromise::Status::Fulfilled + ? static_cast(JSGenerator::ResumeMode::NormalMode) + : static_cast(JSGenerator::ResumeMode::ThrowMode)); + + auto* module = uncheckedDowncast(recordInstance->record()); + JSValue result = module->evaluateInstance(globalObject, recordInstance, resolution, resumeMode); + asyncModuleResolveEvaluation(globalObject, vm, scope, recordInstance, result); +} + static void moduleRegistryFetchSettled(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, std::span arguments, uint8_t payload) { // arguments[0] = pre-created modulePromise @@ -1606,6 +1643,75 @@ static void dynamicImportEvaluateSettled(JSGlobalObject* globalObject, VM& vm, T capabilityPromise->reject(vm, arguments[1]); } +#if USE(BUN_JSC_ADDITIONS) +// import() into a module graph instance (JSModuleLoader::importIntoGraphInstance): +// the load settled. arguments[0] = result promise, [1] = key or error, +// [2] = context object { @moduleGraphInstance, name: key, type, @defer }. +static void moduleGraphInstanceLoadSettled(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, std::span arguments, uint8_t payload) +{ + auto* resultPromise = uncheckedDowncast(arguments[0]); + if (static_cast(payload) != JSPromise::Status::Fulfilled) { + resultPromise->reject(vm, arguments[1]); + return; + } + JSObject* context = asObject(arguments[2]); + auto* instance = uncheckedDowncast(context->getDirect(vm, vm.propertyNames->builtinNames().moduleGraphInstancePrivateName())); + Identifier key = context->getDirect(vm, vm.propertyNames->name).toPropertyKey(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + auto type = static_cast(context->getDirect(vm, vm.propertyNames->type).asInt32()); + bool deferred = context->getDirect(vm, vm.propertyNames->builtinNames().deferPrivateName()).isTrue(); + // resultPromise is the import() promise the importing code awaits: the + // evaluation's top-level-await deadlock check needs it. + JSPromise* namespacePromise = JSModuleLoader::instantiateLoadedModuleIntoGraphInstance(globalObject, key, instance, type, deferred, resultPromise); + if (scope.exception()) [[unlikely]] { + resultPromise->rejectWithCaughtException(vm, scope); + return; + } + resultPromise->pipeFrom(vm, namespacePromise); +} + +// The instance's evaluation of the imported module settled: resolve with the +// namespace object for (record, instance). arguments as above plus context.value = record. +static void moduleGraphInstanceEvaluateSettled(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, std::span arguments, uint8_t payload) +{ + auto* resultPromise = uncheckedDowncast(arguments[0]); + if (static_cast(payload) != JSPromise::Status::Fulfilled) { + resultPromise->reject(vm, arguments[1]); + return; + } + JSObject* context = asObject(arguments[2]); + auto* instance = uncheckedDowncast(context->getDirect(vm, vm.propertyNames->builtinNames().moduleGraphInstancePrivateName())); + auto* record = uncheckedDowncast(context->getDirect(vm, vm.propertyNames->value)); + bool deferred = context->getDirect(vm, vm.propertyNames->builtinNames().deferPrivateName()).isTrue(); + JSModuleNamespaceObject* moduleNamespace = record->getModuleNamespace(globalObject, instance, deferred ? AbstractModuleRecord::ModulePhase::Defer : AbstractModuleRecord::ModulePhase::Evaluation); + if (scope.exception()) [[unlikely]] { + resultPromise->rejectWithCaughtException(vm, scope); + return; + } + resultPromise->resolve(globalObject, vm, moduleNamespace); +} + +#endif + +// AND-join over the asynchronous transitive dependencies of a deferred import +// into an instance (JSModuleRecord::instantiateIntoGraphInstanceAsync). +// arguments[0] = result promise, [1] = value or error, [2] = JSPromiseCombinatorsGlobalContext (count). +static void moduleGraphInstanceDependencySettled(JSGlobalObject* globalObject, VM& vm, ThrowScope&, std::span arguments, uint8_t payload) +{ + auto* resultPromise = uncheckedDowncast(arguments[0]); + auto* joinContext = uncheckedDowncast(arguments[2]); + if (static_cast(payload) != JSPromise::Status::Fulfilled) { + resultPromise->reject(vm, arguments[1]); // first rejection wins + return; + } + uint64_t count = joinContext->remainingElementsCount(); + ASSERT(count > 0); + uint64_t remaining = count - 1; + joinContext->setRemainingElementsCount(remaining); + if (!remaining) + resultPromise->resolve(globalObject, vm, jsUndefined()); +} + static void importModuleNamespace(JSGlobalObject* globalObject, VM& vm, ThrowScope&, std::span arguments, uint8_t payload) { // requestImportModule: namespace getter @@ -1782,8 +1888,14 @@ static void asyncGeneratorDriverResume(VM& vm, JSValue context, JSValue resoluti return; } - // The only remaining for-await driver kind is a top-level-await module. Any other context type - // reaching here means a new driver was wired up without a branch above. + // The only remaining for-await driver kind is a top-level-await module (in the primary graph or + // in a module graph instance). Any other context type reaching here means a new driver was wired up + // without a branch above. + if (auto* recordInstance = dynamicDowncast(context)) { + if (!recordInstance->graphInstance()->isCleared()) + asyncModuleExecutionResume(uncheckedDowncast(recordInstance->record())->realm(), vm, recordInstance, resolution, status); + return; + } auto* module = dynamicDowncast(context); RELEASE_ASSERT(module); asyncModuleExecutionResume(module->realm(), vm, module, resolution, status); @@ -2188,6 +2300,12 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas #endif case InternalMicrotask::AsyncModuleExecutionDone: { + if (auto* recordInstance = dynamicDowncast(arguments[2])) { + if (recordInstance->graphInstance()->isCleared()) + return; // disposed while evaluating: nothing left to complete + auto* module = uncheckedDowncast(recordInstance->record()); + RELEASE_AND_RETURN(scope, asyncModuleExecutionDone(module->realm(), module, arguments[1], static_cast(payload), recordInstance->graphInstance())); + } auto* module = uncheckedDowncast(arguments[2]); RELEASE_AND_RETURN(scope, asyncModuleExecutionDone(module->realm(), module, arguments[1], static_cast(payload))); } @@ -2200,6 +2318,8 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas #if USE(BUN_JSC_ADDITIONS) AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); #endif + if (auto* recordInstance = dynamicDowncast(contextArg)) + RELEASE_AND_RETURN(scope, asyncModuleExecutionResume(uncheckedDowncast(recordInstance->record())->realm(), vm, recordInstance, arguments[1], static_cast(payload))); auto* module = uncheckedDowncast(contextArg); RELEASE_AND_RETURN(scope, asyncModuleExecutionResume(module->realm(), vm, module, arguments[1], static_cast(payload))); } @@ -2299,6 +2419,29 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas return; } + case InternalMicrotask::ModuleGraphInstanceLoadSettled: { +#if USE(BUN_JSC_ADDITIONS) + moduleGraphInstanceLoadSettled(globalObject, vm, scope, arguments, payload); +#else + RELEASE_ASSERT_NOT_REACHED(); +#endif + return; + } + + case InternalMicrotask::ModuleGraphInstanceEvaluateSettled: { +#if USE(BUN_JSC_ADDITIONS) + moduleGraphInstanceEvaluateSettled(globalObject, vm, scope, arguments, payload); +#else + RELEASE_ASSERT_NOT_REACHED(); +#endif + return; + } + + case InternalMicrotask::ModuleGraphInstanceDependencySettled: { + moduleGraphInstanceDependencySettled(globalObject, vm, scope, arguments, payload); + return; + } + case InternalMicrotask::ImportModuleNamespace: { importModuleNamespace(globalObject, vm, scope, arguments, payload); return; diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.h b/Source/JavaScriptCore/runtime/JSMicrotask.h index ed78a91cf17c..3b3fd828a015 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.h +++ b/Source/JavaScriptCore/runtime/JSMicrotask.h @@ -34,11 +34,14 @@ namespace JSC { class MicrotaskCallCache; class JSAsyncGenerator; class JSModuleRecord; +class ModuleRecordInstance; class ThrowScope; void runInternalMicrotask(JSGlobalObject*, VM&, InternalMicrotask, uint8_t, std::span, MicrotaskCallCache* = nullptr); void asyncModuleResolveEvaluation(JSGlobalObject*, VM&, ThrowScope&, JSModuleRecord*, JSValue result); +// Same, for a module body running in a module graph instance (its ModuleRecordInstance). +void asyncModuleResolveEvaluation(JSGlobalObject*, VM&, ThrowScope&, ModuleRecordInstance*, JSValue result); // https://tc39.es/ecma262/#sec-asyncgeneratorresume and #sec-asyncgeneratorawaitreturn — used by the C++ // %AsyncGeneratorPrototype%.return / .throw host functions to drive a non-busy generator. diff --git a/Source/JavaScriptCore/runtime/JSModuleEnvironment.cpp b/Source/JavaScriptCore/runtime/JSModuleEnvironment.cpp index f2f0ce896e14..8f8e3205e9d2 100644 --- a/Source/JavaScriptCore/runtime/JSModuleEnvironment.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleEnvironment.cpp @@ -32,6 +32,9 @@ #include "AbstractModuleRecord.h" #include "JSCInlines.h" #include "JSLexicalEnvironmentInlines.h" +#include "ModuleGraphInstance.h" +#include "JSModuleRecord.h" +#include "SyntheticModuleRecord.h" namespace JSC { @@ -51,11 +54,16 @@ JSModuleEnvironment* JSModuleEnvironment::create( // // JSModuleEnvironment: // [ JSLexicalEnvironment ][ variable slots ][ additional slots for JSModuleEnvironment ] + // ... [ module record ][ graph instance ][ import slots (importSlotCount) ] + unsigned importSlotCount = moduleRecord ? moduleRecord->importSlotCount() : 0; JSModuleEnvironment* result = new ( NotNull, - allocateCell(vm, JSModuleEnvironment::allocationSize(symbolTable))) + allocateCell(vm, JSModuleEnvironment::allocationSize(symbolTable, importSlotCount))) JSModuleEnvironment(vm, structure, currentScope, symbolTable, initialValue, moduleRecord); + result->importSlotCountSlot() = importSlotCount; + for (unsigned i = 0; i < importSlotCount; ++i) + result->importSlot(i).setStartingValue(JSValue()); result->finishCreation(vm); return result; } @@ -68,6 +76,92 @@ void JSModuleEnvironment::visitChildrenImpl(JSCell* cell, Visitor& visitor) Base::visitChildren(thisObject, visitor); visitor.appendValues(thisObject->variables(), thisObject->symbolTable()->scopeSize()); visitor.append(thisObject->moduleRecordSlot()); + visitor.append(thisObject->graphInstanceSlot()); + if (unsigned count = thisObject->importSlotCount()) + visitor.appendValues(std::bit_cast*>(std::bit_cast(thisObject) + offsetOfImportSlot(thisObject->symbolTable(), 0)), count); +} + +void JSModuleEnvironment::fillImportSlots(JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + AbstractModuleRecord* record = moduleRecord(); + if (!record) + return; + UNUSED_PARAM(globalObject); + ModuleGraphInstance* instance = graphInstance(); + ASSERT(importSlotCount() == record->importSlotCount()); + unsigned count = std::min(importSlotCount(), record->importSlotCount()); + for (unsigned i = 0; i < count; ++i) { + if (importSlot(i).get()) + continue; + AbstractModuleRecord* exporter = record->importedRecordAt(i); + JSModuleEnvironment* target = nullptr; + if (instance) { + if (JSModuleEnvironment* found = instance->environment(exporter)) + target = found; + else if (auto* synthetic = dynamicDowncast(exporter); synthetic && !synthetic->hasPerGraphInstanceState()) + target = exporter->moduleEnvironmentMayBeNull(); // stateless synthetic exporters are shared with the primary graph + else if (!dynamicDowncast(exporter) && !dynamicDowncast(exporter)) + target = exporter->moduleEnvironmentMayBeNull(); + } else + target = exporter->moduleEnvironmentMayBeNull(); + if (target) + importSlot(i).set(vm, this, target); + } +} + +JSModuleEnvironment* JSModuleEnvironment::importedEnvironmentFor(JSGlobalObject* globalObject, AbstractModuleRecord* exporter) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ModuleGraphInstance* instance = graphInstance(); + if (!instance) + return exporter->moduleEnvironment(); + // A binding of this module itself resolves to this environment, and an + // import whose slot has been filled resolves through the slot: code that + // runs from an instance keeps resolving within that instance whatever the + // instance map holds by then (an embedder may clear it once the instance + // is no longer wanted for new imports). + AbstractModuleRecord* record = moduleRecord(); + if (record == exporter) + return this; + std::optional slotIndex = record ? record->importSlotIndexFor(exporter) : std::nullopt; + if (slotIndex) { + if (JSValue filled = importSlot(*slotIndex).get(); filled && filled.isCell()) + return uncheckedDowncast(filled); + } + JSModuleEnvironment* environment = exporter->graphInstanceEnvironment(globalObject, instance, true); + RETURN_IF_EXCEPTION(scope, nullptr); + if (!environment) + environment = exporter->moduleEnvironment(); // shared with the primary graph + // Fill the slot so the interpreter and JIT fast paths take over from here. + if (slotIndex && environment) + importSlot(*slotIndex).set(vm, this, environment); + return environment; +} + +JSObject* JSModuleEnvironment::resolveModuleVarScope(JSGlobalObject* globalObject, JSScope* scope, unsigned depth, JSModuleEnvironment* linkedExporter) +{ + // The CodeBlock was linked against one instantiation of the importing + // module; other instantiations share it. The importing module environment + // on THIS scope chain decides which instance's exporter environment applies. + JSScope* cursor = scope; + for (unsigned i = 0; i < depth; ++i) + cursor = cursor->next(); + if (auto* importer = dynamicDowncast(cursor); importer && importer->graphInstance()) + return importer->importedEnvironmentFor(globalObject, linkedExporter->moduleRecord()); + return linkedExporter; +} + +ModuleGraphInstance* JSModuleEnvironment::graphInstance() +{ + JSValue value = graphInstanceSlot().get(); + return value && value.isCell() ? uncheckedDowncast(value.asCell()) : nullptr; +} + +void JSModuleEnvironment::setGraphInstance(VM& vm, ModuleGraphInstance* instance) +{ + graphInstanceSlot().set(vm, this, instance ? JSValue(instance) : JSValue()); } DEFINE_VISIT_CHILDREN(JSModuleEnvironment); @@ -81,7 +175,8 @@ bool JSModuleEnvironment::getOwnPropertySlot(JSObject* cell, JSGlobalObject* glo RETURN_IF_EXCEPTION(scope, false); if (resolution.type == AbstractModuleRecord::Resolution::Type::Resolved) { // When resolveImport resolves the resolution, the imported module environment must have the binding. - JSModuleEnvironment* importedModuleEnvironment = resolution.moduleRecord->moduleEnvironment(); + JSModuleEnvironment* importedModuleEnvironment = thisObject->importedEnvironmentFor(globalObject, resolution.moduleRecord); + RETURN_IF_EXCEPTION(scope, false); PropertySlot redirectSlot(importedModuleEnvironment, PropertySlot::InternalMethodType::Get); bool result = importedModuleEnvironment->methodTable()->getOwnPropertySlot(importedModuleEnvironment, globalObject, resolution.localName, redirectSlot); ASSERT_UNUSED(result, result); diff --git a/Source/JavaScriptCore/runtime/JSModuleEnvironment.h b/Source/JavaScriptCore/runtime/JSModuleEnvironment.h index 4fc646ba9ae7..103ca3c36020 100644 --- a/Source/JavaScriptCore/runtime/JSModuleEnvironment.h +++ b/Source/JavaScriptCore/runtime/JSModuleEnvironment.h @@ -35,6 +35,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN namespace JSC { class AbstractModuleRecord; +class ModuleGraphInstance; class Register; class JSModuleEnvironment final : public JSLexicalEnvironment { @@ -63,16 +64,69 @@ class JSModuleEnvironment final : public JSLexicalEnvironment { return offset; } - static size_t allocationSize(SymbolTable* symbolTable) + static size_t offsetOfGraphInstance(SymbolTable* symbolTable) { return offsetOfModuleRecord(symbolTable) + sizeof(WriteBarrier); } + // Number of import slots this environment was allocated with (raw word). + static size_t offsetOfImportSlotCount(SymbolTable* symbolTable) + { + return offsetOfGraphInstance(symbolTable) + sizeof(WriteBarrier); + } + + static size_t offsetOfImportSlot(SymbolTable* symbolTable, unsigned index) + { + return offsetOfImportSlotCount(symbolTable) + sizeof(uintptr_t) + sizeof(WriteBarrier) * index; + } + // Import slot `index` addressed as a variable of this environment (past the + // symbol table's own variables), so JITs can treat it like a closure variable. + static ScopeOffset importSlotScopeOffset(SymbolTable* symbolTable, unsigned index) + { + size_t byteOffset = offsetOfImportSlot(symbolTable, index) - offsetOfVariables(); + ASSERT(!(byteOffset % sizeof(WriteBarrier))); + return ScopeOffset(byteOffset / sizeof(WriteBarrier)); + } + + static size_t allocationSize(SymbolTable* symbolTable, unsigned importSlotCount = 0) + { + return offsetOfImportSlot(symbolTable, importSlotCount); + } + + unsigned importSlotCount() { return static_cast(importSlotCountSlot()); } + WriteBarrierBase& importSlot(unsigned index) + { + RELEASE_ASSERT(index < importSlotCount()); + return *std::bit_cast*>(std::bit_cast(this) + offsetOfImportSlot(symbolTable(), index)); + } + // Point every import slot at the exporter's environment in this graph + // instance (or the exporter's primary environment). Slots whose exporter has + // no environment yet (link-time cycles) stay empty and are filled on first use. + void fillImportSlots(JSGlobalObject*); + AbstractModuleRecord* moduleRecord() { return moduleRecordSlot().get(); } + // Module graph instances (prototype): a secondary instantiation of a module + // graph shares every record, executable and CodeBlock with the primary one + // and differs only in its environments. Each secondary environment points at + // its instance's map (a JSMap: AbstractModuleRecord → JSModuleEnvironment) + // so ModuleVar resolution can find the importing instance's copy of the + // exporting module's environment. Null on primary environments. + // The module graph instance this environment belongs to (null: the primary instantiation). + ModuleGraphInstance* graphInstance(); + void setGraphInstance(VM&, ModuleGraphInstance*); + // The environment of `exporter` in the same graph instance as this one + // (the exporter's primary environment if this is a primary environment). + JSModuleEnvironment* importedEnvironmentFor(JSGlobalObject*, AbstractModuleRecord* exporter); + // op_resolve_scope slow path for a ModuleVar under module graph instances: + // walk `depth` scopes from `scope` to the importing module environment and + // return the exporter's environment in that environment's instance + // (`linkedExporter` — the one the CodeBlock was linked against — otherwise). + static JSObject* resolveModuleVarScope(JSGlobalObject*, JSScope*, unsigned depth, JSModuleEnvironment* linkedExporter); + static bool getOwnPropertySlot(JSObject*, JSGlobalObject*, PropertyName, PropertySlot&); static void getOwnSpecialPropertyNames(JSObject*, JSGlobalObject*, PropertyNameArrayBuilder&, DontEnumPropertiesMode); static bool put(JSCell*, JSGlobalObject*, PropertyName, JSValue, PutPropertySlot&); @@ -89,12 +143,22 @@ class JSModuleEnvironment final : public JSLexicalEnvironment { { return *std::bit_cast*>(std::bit_cast(this) + offsetOfModuleRecord(symbolTable())); } + WriteBarrierBase& graphInstanceSlot() + { + return *std::bit_cast*>(std::bit_cast(this) + offsetOfGraphInstance(symbolTable())); + } + uintptr_t& importSlotCountSlot() + { + return *std::bit_cast(std::bit_cast(this) + offsetOfImportSlotCount(symbolTable())); + } }; inline JSModuleEnvironment::JSModuleEnvironment(VM& vm, Structure* structure, JSScope* currentScope, SymbolTable* symbolTable, JSValue initialValue, AbstractModuleRecord* moduleRecord) : Base(vm, structure, currentScope, symbolTable, initialValue) { this->moduleRecordSlot().setWithoutWriteBarrier(moduleRecord); + this->graphInstanceSlot().setWithoutWriteBarrier(JSValue()); + this->importSlotCountSlot() = 0; } } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 53980b04f39f..2ce70ad6a052 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -35,6 +35,11 @@ #include "JSMicrotask.h" #include "JSModuleNamespaceObject.h" #include "JSModuleRecord.h" +#include "ModuleGraphInstance.h" +#include "StrongInlines.h" +#include "JSModuleEnvironment.h" +#include "JSMapInlines.h" +#include "JSNativeStdFunction.h" #include "JSPromise.h" #include "JSSourceCode.h" #include "JSWebAssembly.h" @@ -364,7 +369,7 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi if (entry->fetchError()) removeFailedFetchEntry(entry); else { - JSValue error = entry->error(globalObject); + JSValue error = entry->error(globalObject, flags.contains(ModuleLoadFlag::ForGraphInstance) ? ModuleRegistryEntry::IncludeEvaluationError::No : ModuleRegistryEntry::IncludeEvaluationError::Yes); RETURN_IF_EXCEPTION(scope, nullptr); if (error) return JSPromise::rejectedPromise(globalObject, error); @@ -451,6 +456,172 @@ static String moduleReferrer(const Identifier& referrerKey) return referrerKey.string(); } +#if USE(BUN_JSC_ADDITIONS) +JSPromise* JSModuleLoader::loadModuleForGraphInstance(JSGlobalObject* globalObject, const Identifier& key, RefPtr&& parameters, ModuleGraphInstance* instance) +{ + VM& vm = globalObject->vm(); + if (instance->isCleared()) + return JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Module graph instance has been disposed"_s)); + // A registry entry whose fetch or instantiation failed is another program + // run's failure (possibly produced on behalf of another instance, e.g. by a + // host module provider that runs code at fetch time): this instance loads + // afresh. Evaluation failures stay (instances evaluate separately anyway). + { + JSModuleLoader* loader = globalObject->moduleLoader(); + auto type = parameters ? parameters->type() : ScriptFetchParameters::Type::JavaScript; + Locker locker { loader->cellLock() }; + if (ModuleRegistryEntry* entry = loader->getRegisteredMayBeNull(key, type)) { + if (entry->hasSettledFailure()) + loader->removeEntry(key); + } + } + auto scope = DECLARE_THROW_SCOPE(vm); + JSPromise* promise = nullptr; + { + JSGlobalObject::GraphInstanceLoadingScope loading(globalObject, instance); + promise = globalObject->moduleLoader()->loadModuleSync(globalObject, key, WTF::move(parameters), nullptr, { ModuleLoadFlag::ForGraphInstance }); + } + RELEASE_AND_RETURN(scope, promise); +} + +// The continuations of an instance import() are internal microtasks +// (InternalMicrotask::ModuleGraphInstanceLoadSettled / EvaluateSettled in +// JSMicrotask.cpp): nothing script-observable (no species lookup, no function +// objects), and nothing Strong<>-rooted, so an evaluation that never settles +// does not keep the instance alive. +JSObject* JSModuleLoader::createGraphInstanceImportContext(JSGlobalObject* globalObject, ModuleGraphInstance* instance, const Identifier& key, ScriptFetchParameters::Type type, bool deferred) +{ + VM& vm = globalObject->vm(); + JSObject* context = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + context->putDirect(vm, vm.propertyNames->builtinNames().moduleGraphInstancePrivateName(), instance); + context->putDirect(vm, vm.propertyNames->name, identifierToJSValue(vm, key)); + context->putDirect(vm, vm.propertyNames->type, jsNumber(static_cast(type))); + context->putDirect(vm, vm.propertyNames->builtinNames().deferPrivateName(), jsBoolean(deferred)); + return context; +} + +JSPromise* JSModuleLoader::instantiateLoadedModuleIntoGraphInstance(JSGlobalObject* globalObject, const Identifier& key, ModuleGraphInstance* instance, ScriptFetchParameters::Type type, bool deferred, JSPromise* dynamicImportPromise) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (instance->isCleared()) + RELEASE_AND_RETURN(scope, JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Module graph instance has been disposed"_s))); + // The loading instance is current only while linking (host-provided + // synthetic modules consult it); evaluating the instance below runs script, + // and loads that script triggers must not be attributed to this instance. + AbstractModuleRecord* record = nullptr; + { + JSGlobalObject::GraphInstanceLoadingScope loading(globalObject, instance); + record = globalObject->moduleLoader()->linkWithoutEvaluating(globalObject, key, nullptr, type); + } + RETURN_IF_EXCEPTION(scope, nullptr); + auto phase = deferred ? AbstractModuleRecord::ModulePhase::Defer : AbstractModuleRecord::ModulePhase::Evaluation; + auto* sourceRecord = dynamicDowncast(record); + if (!sourceRecord) { + if (is(record)) { + // As for a static import (JSModuleRecord::createInstanceEnvironment): + // only Source Text Module Records can be instantiated per instance, + // and sharing the primary's copy of another Cyclic Module Record + // (WebAssembly) would hand out a namespace nobody evaluated. + throwTypeError(globalObject, scope, makeString("Module '"_s, key.string(), "' cannot be instantiated into a module graph instance (only JavaScript and synthetic modules can)"_s)); + return nullptr; + } + // Synthetic modules: an environment in the instance when they carry + // per-instance state, otherwise shared with the primary graph. + JSModuleNamespaceObject* ns = record->getModuleNamespace(globalObject, instance, phase); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, JSPromise::resolvedPromise(globalObject, ns)); + } + JSPromise* evaluated = sourceRecord->instantiateIntoGraphInstanceAsync(globalObject, instance, phase, dynamicImportPromise); + RETURN_IF_EXCEPTION(scope, nullptr); + JSObject* context = createGraphInstanceImportContext(globalObject, instance, key, type, deferred); + context->putDirect(vm, vm.propertyNames->value, sourceRecord); + JSPromise* result = JSPromise::create(vm, globalObject->promiseStructure()); + evaluated->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleGraphInstanceEvaluateSettled, result, context); + return result; +} + +JSPromise* JSModuleLoader::importIntoGraphInstance(JSGlobalObject* globalObject, JSString* specifierValue, JSValue parameters, const SourceOrigin& referrer, ModuleGraphInstance* instance, bool deferred) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (instance->isCleared()) + RELEASE_AND_RETURN(scope, JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Module graph instance has been disposed"_s))); + JSPromise* result = JSPromise::create(vm, globalObject->promiseStructure()); + + auto specifier = specifierValue->value(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + // Import attributes (`with { type }`) select the fetch parameters like a + // regular import() does. + RefPtr fetchParameters; + { + auto attributes = retrieveImportAttributesFromDynamicImportOptions(globalObject, parameters, { vm.propertyNames->type.impl() }); + if (scope.exception()) [[unlikely]] { + result->rejectWithCaughtException(vm, scope); + RELEASE_AND_RETURN(scope, result); + } + auto type = retrieveTypeImportAttribute(globalObject, attributes); + if (scope.exception()) [[unlikely]] { + result->rejectWithCaughtException(vm, scope); + RELEASE_AND_RETURN(scope, result); + } + if (type) { +#if USE(BUN_JSC_ADDITIONS) + if (type.value() == ScriptFetchParameters::Type::HostDefined) + fetchParameters = ScriptFetchParameters::create(attributes.get(vm.propertyNames->type.impl())); + else +#endif + fetchParameters = ScriptFetchParameters::create(type.value()); + } + } + Identifier referrerKey = referrer.url().isValid() ? Identifier::fromString(vm, referrer.url().fileSystemPath()) : Identifier::fromString(vm, referrer.string()); + Identifier key = globalObject->moduleLoader()->resolve(globalObject, Identifier::fromString(vm, specifier), referrerKey, nullptr, false); + if (scope.exception()) [[unlikely]] { + result->rejectWithCaughtException(vm, scope); + RELEASE_AND_RETURN(scope, result); + } + auto fetchType = fetchParameters ? fetchParameters->type() : ScriptFetchParameters::Type::JavaScript; + // Fetch + parse synchronously with this graph as the loading instance, so + // host-side module objects created on the way (CommonJS behind an import) + // belong to this graph. Evaluation stays asynchronous. + JSPromise* loaded = loadModuleForGraphInstance(globalObject, key, WTF::move(fetchParameters), instance); + RETURN_IF_EXCEPTION(scope, nullptr); + JSObject* context = createGraphInstanceImportContext(globalObject, instance, key, fetchType, deferred); + loaded->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleGraphInstanceLoadSettled, result, context); + return result; +} + +#endif // USE(BUN_JSC_ADDITIONS) + +AbstractModuleRecord* JSModuleLoader::linkWithoutEvaluating(JSGlobalObject* globalObject, const Identifier& moduleKey, RefPtr scriptFetcher, ScriptFetchParameters::Type type) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ModuleRegistryEntry* entry = ensureRegistered(globalObject, moduleKey, type); + RETURN_IF_EXCEPTION(scope, nullptr); + AbstractModuleRecord* record = entry->record(); + if (!record) { + throwTypeError(globalObject, scope, makeString("Module '"_s, moduleKey.string(), "' has not been fetched"_s)); + return nullptr; + } + // Only a fetch or instantiation failure makes the template unusable; the + // primary graph's evaluation error is its own (an instance evaluates the + // record separately, with its own [[EvaluationError]]). + JSValue error = entry->error(globalObject, ModuleRegistryEntry::IncludeEvaluationError::No); + RETURN_IF_EXCEPTION(scope, nullptr); + if (error) { + scope.throwException(globalObject, error); + return nullptr; + } + record->link(globalObject, WTF::move(scriptFetcher)); + if (Exception* exception = scope.exception()) { + attachErrorInfo(globalObject, scope, record, entry->key(), entry->moduleType(), ModuleFailure::Kind::Instantiation); + entry->setInstantiationError(globalObject, exception->value()); + return nullptr; + } + return record; +} + JSPromise* JSModuleLoader::requestImportModule(JSGlobalObject* globalObject, const Identifier& moduleName, const Identifier& referrer, RefPtr parameters, RefPtr scriptFetcher, bool deferred, int64_t referrerAsyncOrder) { VM& vm = globalObject->vm(); @@ -801,7 +972,14 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, if (mapEntry->status() == ModuleRegistryEntry::Status::New) { // Per "fetch the descendants of a module script", the referrer is the referring module's base URL. JSPromise* promise = fetch(globalObject, identifierToJSValue(vm, resolved), moduleReferrer(referrerKey), moduleRequest.m_attributes, scriptFetcher); - RETURN_IF_EXCEPTION(scope, nullptr); + if (Exception* exception = scope.exception()) [[unlikely]] { + // A host fetch hook that throws synchronously: record the failure on + // the entry (a later load of this key sees it, or retries after the + // entry is dropped) rather than leaving a New entry in the map. + if (!vm.isTerminationException(exception)) + mapEntry->setFetchError(globalObject, exception->value()); + return nullptr; + } mapEntry->setStatus(ModuleRegistryEntry::Status::Fetching); mapEntry->ensureFetchPromise(globalObject)->pipeFrom(vm, promise); @@ -1236,7 +1414,7 @@ void JSModuleLoader::drainSynchronousModuleQueue(JSGlobalObject* globalObject) tasks.shrink(0); } -JSPromise* JSModuleLoader::loadModuleSync(JSGlobalObject* globalObject, const Identifier& moduleName, RefPtr&& parameters, RefPtr&& scriptFetcher) +JSPromise* JSModuleLoader::loadModuleSync(JSGlobalObject* globalObject, const Identifier& moduleName, RefPtr&& parameters, RefPtr&& scriptFetcher, OptionSet flags) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -1248,7 +1426,7 @@ JSPromise* JSModuleLoader::loadModuleSync(JSGlobalObject* globalObject, const Id vm.m_synchronousModuleQueue = queue.prev; }); - JSPromise* result = loadModule(globalObject, moduleName, WTF::move(parameters), WTF::move(scriptFetcher), { ModuleLoadFlag::Evaluate }); + JSPromise* result = loadModule(globalObject, moduleName, WTF::move(parameters), WTF::move(scriptFetcher), flags); RETURN_IF_EXCEPTION(scope, result); scope.release(); @@ -1309,9 +1487,42 @@ JSPromise* JSModuleLoader::makeModule(JSGlobalObject* globalObject, const Identi Vector exportNames; JSObject* lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args); RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); + if (args.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return promise->rejectWithCaughtException(vm, scope); + } - auto* moduleRecord = SyntheticModuleRecord::tryCreateWithExportNamesAndValues(globalObject, moduleKey, exportNames, args, lazyExportsSource); + ModuleGraphInstance* loadingInstance = syntheticSourceProvider->regeneratesPerGraphInstance() ? globalObject->currentGraphInstanceForLoading() : nullptr; + MarkedArgumentBuffer primaryValues; + if (loadingInstance) { + // Generated on behalf of a graph: those values are the graph's; the + // primary's are produced later, if the primary ever links to them. + for (unsigned i = 0; i < args.size(); ++i) + primaryValues.append(JSValue()); + if (primaryValues.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return promise->rejectWithCaughtException(vm, scope); + } + } + auto* moduleRecord = SyntheticModuleRecord::tryCreateWithExportNamesAndValues(globalObject, moduleKey, exportNames, loadingInstance ? primaryValues : args, lazyExportsSource ? lazyExportsSource : (loadingInstance ? globalObject->globalThis() : nullptr)); RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); + if (moduleRecord) { + moduleRecord->setSyntheticSourceProvider(syntheticSourceProvider, !!loadingInstance); + if (loadingInstance) { + JSModuleEnvironment* environment = JSModuleEnvironment::create(vm, globalObject, nullptr, moduleRecord->moduleEnvironment()->symbolTable(), jsTDZValue(), moduleRecord); + RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); + for (unsigned i = 0; i < exportNames.size(); ++i) { + bool putResult = false; + symbolTablePutTouchWatchpointSet(environment, globalObject, exportNames[i], args.at(i) ? args.at(i) : jsUndefined(), false, true, putResult); + RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); + } + // The instance may have been disposed while the host generated + // this module (that ran script): the record is still created (it + // is the primary's too), just not filed under the instance. + if (!loadingInstance->isCleared()) + loadingInstance->add(vm, moduleRecord, environment); + } + } scope.release(); promise->fulfill(vm, moduleRecord); diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.h b/Source/JavaScriptCore/runtime/JSModuleLoader.h index a422b279dd0f..746feb95b836 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.h +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.h @@ -39,8 +39,11 @@ namespace JSC { class ErrorInstance; class JSPromise; class JSModuleNamespaceObject; +class ModuleGraphInstance; class JSModuleRecord; class JSSourceCode; +class JSMap; +class JSModuleEnvironment; class ModuleRegistryEntry; class SourceOrigin; @@ -49,6 +52,9 @@ enum class ModuleLoadFlag : uint8_t { Dynamic = 1 << 1, UseImportMap = 1 << 2, Deferred = 1 << 3, + // Loading a template for a module graph instance: the primary graph's + // evaluation error for an already-loaded module is not this load's failure. + ForGraphInstance = 1 << 4, }; class JSModuleLoader final : public JSCell { @@ -94,6 +100,20 @@ class JSModuleLoader final : public JSCell { void provideFetch(JSGlobalObject*, const Identifier& key, ScriptFetchParameters::Type, JSSourceCode*); JSPromise* loadModule(JSGlobalObject*, const Identifier& moduleName, RefPtr, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1, const String& referrer = { }); JSPromise* linkAndEvaluateModule(JSGlobalObject*, const Identifier& moduleKey, RefPtr, RefPtr); + // Module graph instances (prototype): link a fetched module graph without + // evaluating it, so it can serve as the template for instantiateIntoGraphInstance. + JS_EXPORT_PRIVATE AbstractModuleRecord* linkWithoutEvaluating(JSGlobalObject*, const Identifier& moduleKey, RefPtr, ScriptFetchParameters::Type = ScriptFetchParameters::Type::JavaScript); +#if USE(BUN_JSC_ADDITIONS) + // import() from code of a module graph instance: fetch and link the graph as + // a template (through the synchronous loader), instantiate it into the + // instance, and resolve with the instance's namespace object. (An + // asynchronous-loader form is needed for ports without loadModuleSync.) + JS_EXPORT_PRIVATE static JSPromise* importIntoGraphInstance(JSGlobalObject*, JSString* specifier, JSValue parameters, const SourceOrigin& referrer, ModuleGraphInstance*, bool deferred = false); + JS_EXPORT_PRIVATE static JSPromise* loadModuleForGraphInstance(JSGlobalObject*, const Identifier& key, RefPtr&&, ModuleGraphInstance*); + // Resolves with the instance's namespace object once its (possibly asynchronous) evaluation completes. + JS_EXPORT_PRIVATE static JSPromise* instantiateLoadedModuleIntoGraphInstance(JSGlobalObject*, const Identifier& key, ModuleGraphInstance*, ScriptFetchParameters::Type = ScriptFetchParameters::Type::JavaScript, bool deferred = false, JSPromise* dynamicImportPromise = nullptr); + static JSObject* createGraphInstanceImportContext(JSGlobalObject*, ModuleGraphInstance*, const Identifier& key, ScriptFetchParameters::Type, bool deferred); +#endif JSPromise* requestImportModule(JSGlobalObject*, const Identifier& moduleName, const Identifier& referrer, RefPtr, RefPtr, bool deferred = false, int64_t referrerAsyncOrder = -1); #if USE(BUN_JSC_ADDITIONS) JS_EXPORT_PRIVATE int64_t asyncEvaluationOrderForKey(const Identifier& key); @@ -208,7 +228,7 @@ class JSModuleLoader final : public JSCell { m_moduleMap.clear(); m_resolutionFailures.clear(); } - JS_EXPORT_PRIVATE JSPromise* loadModuleSync(JSGlobalObject*, const Identifier& moduleName, RefPtr&&, RefPtr&&); + JS_EXPORT_PRIVATE JSPromise* loadModuleSync(JSGlobalObject*, const Identifier& moduleName, RefPtr&&, RefPtr&&, OptionSet = { ModuleLoadFlag::Evaluate }); JS_EXPORT_PRIVATE static void drainSynchronousModuleQueue(JSGlobalObject*); #endif diff --git a/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.cpp b/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.cpp index 03d40b0d33e4..7acb6e5cdb1a 100644 --- a/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.cpp @@ -29,7 +29,9 @@ #include "AbstractModuleRecord.h" #include "CyclicModuleRecord.h" #include "JSCInlines.h" +#include #include "JSModuleEnvironment.h" +#include "ModuleGraphInstance.h" #include "JSModuleRecord.h" #if USE(BUN_JSC_ADDITIONS) #include "SyntheticModuleRecord.h" @@ -102,12 +104,44 @@ void JSModuleNamespaceObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_moduleRecord); + visitor.append(thisObject->m_graphInstance); + visitor.append(thisObject->m_instanceEnvironment); for (auto& entry : thisObject->m_exports.values()) visitor.appendHidden(entry.moduleRecord); } DEFINE_VISIT_CHILDREN(JSModuleNamespaceObject); +void JSModuleNamespaceObject::setGraphInstance(VM& vm, ModuleGraphInstance* instance, JSModuleEnvironment* environment) +{ + m_graphInstance.setMayBeNull(vm, this, instance); + m_instanceEnvironment.setMayBeNull(vm, this, environment); +} + +JSModuleEnvironment* JSModuleNamespaceObject::environmentFor(JSGlobalObject* globalObject, AbstractModuleRecord* record) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (ModuleGraphInstance* instance = m_graphInstance.get()) { + // Own exports: this instance's environment; re-exports: through its + // import slots; then the instance. A namespace stays bound to the + // instance it was created for. + if (JSModuleEnvironment* own = m_instanceEnvironment.get()) { + if (record == m_moduleRecord.get()) + return own; + RELEASE_AND_RETURN(scope, own->importedEnvironmentFor(globalObject, record)); + } + if (JSModuleEnvironment* found = instance->environment(record)) + return found; + if (instance->isCleared()) { + throwTypeError(globalObject, scope, "Module namespace belongs to a module graph instance that was disposed"_s); + return nullptr; + } + // Otherwise the record is one the instance shares with the primary graph. + } + return record->moduleEnvironment(); +} + // https://tc39.es/proposal-defer-import-eval/#sec-IsSymbolLikeNamespaceKey ALWAYS_INLINE bool JSModuleNamespaceObject::isSymbolLikeNamespaceKey(VM& vm, PropertyName propertyName) { @@ -121,18 +155,26 @@ void JSModuleNamespaceObject::ensureDeferredNamespaceEvaluation(JSGlobalObject* { // 1. If O.[[Deferred]] is true, then ASSERT(m_isDeferred); + // A namespace of a module graph instance evaluates its module in that instance. + ModuleGraphInstance* instance = m_graphInstance.get(); + if (instance && instance->isCleared()) { + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "Module namespace belongs to a module graph instance that was disposed"_s); + return; + } // Fast path: if the module's cycle has already successfully evaluated, EvaluateModuleSync would // observe a fulfilled promise and return without throwing, so we can skip the work entirely. // We must consult [[CycleRoot]] here because Evaluate() redirects to it; for a non-root SCC // member, status/evaluationError on the module itself may not reflect the cycle's outcome. if (auto* cyclic = dynamicDowncast(m_moduleRecord.get())) { - CyclicModuleRecord* root = cyclic->cycleRoot() ? cyclic->cycleRoot() : cyclic; - if (root->status() == CyclicModuleRecord::Status::Evaluated && !root->evaluationError()) + CyclicModuleRecord* root = cyclic->cycleRoot(instance) ? cyclic->cycleRoot(instance) : cyclic; + if (root->status(instance) == CyclicModuleRecord::Status::Evaluated && !root->evaluationError(instance)) return; } // 1.a. Let m be O.[[Module]]. // 1.b. Perform ? EvaluateModuleSync(m). - m_moduleRecord->evaluateSync(globalObject); + m_moduleRecord->evaluateSync(globalObject, instance); // 2. Return O.[[Exports]]. } @@ -189,10 +231,11 @@ bool JSModuleNamespaceObject::getOwnPropertySlotCommon(JSGlobalObject* globalObj // 10. If binding.[[BindingName]] is "*namespace*", then // a. Return ? GetModuleNamespace(targetModule). // We call getModuleNamespace() to ensure materialization. And after that, looking up the value from the scope to encourage module namespace object IC. - exportEntry.moduleRecord->getModuleNamespace(globalObject); + exportEntry.moduleRecord->getModuleNamespace(globalObject, m_graphInstance.get()); RETURN_IF_EXCEPTION(scope, false); } - JSModuleEnvironment* environment = exportEntry.moduleRecord->moduleEnvironment(); + JSModuleEnvironment* environment = environmentFor(globalObject, exportEntry.moduleRecord.get()); + RETURN_IF_EXCEPTION(scope, false); ScopeOffset scopeOffset; JSValue value = getValue(environment, exportEntry.localName, scopeOffset); #if USE(BUN_JSC_ADDITIONS) @@ -466,19 +509,22 @@ bool JSModuleNamespaceObject::overrideExportValue(JSGlobalObject* globalObject, } auto* record = resolution.moduleRecord; - auto* moduleNamespaceObject = record->getModuleNamespace(globalObject); + // A namespace of a module graph instance overrides within that instance. + ModuleGraphInstance* instance = m_graphInstance.get(); + auto* moduleNamespaceObject = instance ? record->getModuleNamespace(globalObject, instance) : record->getModuleNamespace(globalObject); RETURN_IF_EXCEPTION(scope, false); bool putResult = false; - moduleNamespaceObject->m_isOverridingValue = true; - if (JSModuleEnvironment* moduleEnvironment = record->moduleEnvironmentMayBeNull()) { + SetForScope overridingValue(moduleNamespaceObject->m_isOverridingValue, true); + JSModuleEnvironment* moduleEnvironment = instance ? environmentFor(globalObject, record) : record->moduleEnvironmentMayBeNull(); + RETURN_IF_EXCEPTION(scope, {}); + if (moduleEnvironment) { symbolTablePutTouchWatchpointSet(moduleEnvironment, globalObject, resolution.localName, value, false, true, putResult); RETURN_IF_EXCEPTION(scope, {}); } JSC::PutPropertySlot putter = JSC::PutPropertySlot(moduleNamespaceObject, false); putResult = moduleNamespaceObject->put(moduleNamespaceObject, globalObject, name, value, putter); RETURN_IF_EXCEPTION(scope, {}); - moduleNamespaceObject->m_isOverridingValue = false; return putResult; } diff --git a/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.h b/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.h index 9e3e15b746a7..7274bd77586e 100644 --- a/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.h +++ b/Source/JavaScriptCore/runtime/JSModuleNamespaceObject.h @@ -31,6 +31,10 @@ namespace JSC { +class ModuleGraphInstance; + +class JSModuleEnvironment; + class JSModuleNamespaceObject final : public JSNonFinalObject { public: using Base = JSNonFinalObject; @@ -73,6 +77,11 @@ class JSModuleNamespaceObject final : public JSNonFinalObject { inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); AbstractModuleRecord* moduleRecord() LIFETIME_BOUND { return m_moduleRecord.get(); } + // Module graph instances (prototype): when set (a JSMap record → environment), + // bindings are read from that instance's environments instead of the records'. + ModuleGraphInstance* graphInstance() const { return m_graphInstance.get(); } + void setGraphInstance(VM&, ModuleGraphInstance*, JSModuleEnvironment*); + JSModuleEnvironment* environmentFor(JSGlobalObject*, AbstractModuleRecord*); #if USE(BUN_JSC_ADDITIONS) WTF::TriState m_hasESModuleMarker = WTF::TriState::Indeterminate; @@ -94,6 +103,10 @@ class JSModuleNamespaceObject final : public JSNonFinalObject { ExportMap m_exports; WriteBarrier m_moduleRecord; + WriteBarrier m_graphInstance; + // This namespace's own module environment in that instance: local exports and + // (through its import slots) re-exports resolve here without the instance map. + WriteBarrier m_instanceEnvironment; const bool m_isDeferred; #if USE(BUN_JSC_ADDITIONS) bool m_isOverridingValue = false; diff --git a/Source/JavaScriptCore/runtime/JSModuleRecord.cpp b/Source/JavaScriptCore/runtime/JSModuleRecord.cpp index 300745243adb..00b8432f6354 100644 --- a/Source/JavaScriptCore/runtime/JSModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleRecord.cpp @@ -27,7 +27,14 @@ #include "JSModuleRecord.h" #include "BuiltinNames.h" +#include "IdentifierInlines.h" #include "Interpreter.h" +#include +#include +#include "StrongInlines.h" +#include "JSGenerator.h" +#include "JSNativeStdFunction.h" +#include "InternalFieldTuple.h" #include "JSAsyncFunction.h" #include "JSAsyncGeneratorFunction.h" #include "JSCInlines.h" @@ -36,8 +43,15 @@ #include "JSModuleEnvironment.h" #include "JSModuleLoader.h" #include "JSModuleNamespaceObject.h" +#include "ModuleGraphInstance.h" +#include "JSMapInlines.h" +#include "JSLexicalEnvironmentInlines.h" +#include "SymbolTableInlines.h" +#include "SyntheticModuleRecord.h" #include "JSPromise.h" +#include "JSPromiseCombinatorsGlobalContext.h" #include "ModuleProgramExecutable.h" +#include "ModuleProgramCodeBlock.h" #include "SourceProfiler.h" #include "UnlinkedModuleProgramCodeBlock.h" #include @@ -92,6 +106,13 @@ void JSModuleRecord::visitChildrenImpl(JSCell* cell, Visitor& visitor) ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_moduleProgramExecutable); + visitor.append(thisObject->m_retainedExecutable); + visitor.append(thisObject->m_retainedCodeBlock); + { + Locker locker { thisObject->cellLock() }; + for (auto& barrier : thisObject->m_functionDeclExecutables) + visitor.append(barrier); + } #if USE(BUN_JSC_ADDITIONS) visitor.reportExtraMemoryVisited(thisObject->sourceCode().memoryCost()); @@ -108,14 +129,27 @@ bool JSModuleRecord::isTopLevelExecutionFinished() const JSValue JSModuleRecord::evaluate(JSGlobalObject* globalObject, JSValue sentValue, JSValue resumeMode) { + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Module graph instances: by the time a module evaluates, its dependencies' + // environments exist; fill the import slots the fast paths read. + if (m_moduleEnvironment && m_moduleEnvironment->importSlotCount() && internalField(Field::State).get() == jsNumber(static_cast(State::Init))) { + for (unsigned i = 0; i < importSlotCount(); ++i) { + if (auto* synthetic = dynamicDowncast(importedRecordAt(i))) { + synthetic->materializePrimaryIfPending(globalObject); + RETURN_IF_EXCEPTION(scope, { }); + } + } + m_moduleEnvironment->fillImportSlots(globalObject); + RETURN_IF_EXCEPTION(scope, { }); + } + if (!m_moduleProgramExecutable) { ASSERT_NOT_REACHED_WITH_MESSAGE("Can't evaluate a JSModuleRecord that has no executable"); return jsUndefined(); } - VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - if (JSValue error = evaluationError()) { scope.throwException(globalObject, error); return { }; @@ -125,6 +159,8 @@ JSValue JSModuleRecord::evaluate(JSGlobalObject* globalObject, JSValue sentValue JSValue resultOrAwaitedValue = vm.interpreter.executeModuleProgram(this, executable, globalObject, moduleEnvironment(), sentValue, resumeMode); RETURN_IF_EXCEPTION(scope, { }); + if (m_retainedExecutable) + pinRetainedCodeBlock(vm); if (isTopLevelExecutionFinished()) m_moduleProgramExecutable.clear(); @@ -172,6 +208,351 @@ void JSModuleRecord::execute(JSGlobalObject* globalObject, JSPromise* capability // 11. Return unused. } +void JSModuleRecord::retainForGraphInstances(VM& vm, ModuleProgramExecutable* executable, Vector>&& functionDeclExecutables) +{ + m_retainedExecutable.set(vm, this, executable); + pinRetainedCodeBlock(vm); + { + // The concurrent marker iterates m_functionDeclExecutables under the cell lock. + Locker locker { cellLock() }; + m_functionDeclExecutables = WTF::move(functionDeclExecutables); + } + for (auto& barrier : m_functionDeclExecutables) { + if (barrier) + vm.writeBarrier(this, barrier.get()); + } +} + +// InitializeEnvironment steps 5-24 against a fresh environment that belongs to +// `instance`, reusing everything the primary instantiation linked. Recursively +// instantiates every source text dependency into the instance first. +JSModuleEnvironment* JSModuleRecord::createInstanceEnvironment(JSGlobalObject* globalObject, ModuleGraphInstance* instance, Vector& created) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (instance->isCleared()) { + throwTypeError(globalObject, scope, "Module graph instance was disposed"_s); + return nullptr; + } + + if (JSModuleEnvironment* existing = instance->environment(this)) + return existing; + + if (!vm.isSafeToRecurseSoft()) [[unlikely]] { + throwStackOverflowError(globalObject, scope); + return nullptr; + } + // Evaluate() step 2 for the template: it must have completed Link(). + if (status() == Status::New || status() == Status::Unlinked || status() == Status::Linking) { + throwTypeError(globalObject, scope, makeString("Module '"_s, moduleKey().string(), "' is not linked and cannot be instantiated into a module graph instance"_s)); + return nullptr; + } + ModuleProgramExecutable* executable = m_retainedExecutable.get(); + if (!executable) { + throwTypeError(globalObject, scope, makeString("Module '"_s, moduleKey().string(), "' cannot be instantiated again: it was linked before module graph instances were enabled"_s)); + return nullptr; + } + + SymbolTable* symbolTable = executable->moduleEnvironmentSymbolTable(); + // The record's CodeBlocks are shared by every instance and were linked + // against the primary environment's scope chain, so an instance's parent + // scope must have the same shape: the global's module environment parent + // scope, or an overlay from createModuleScopeOverlay when the primary graph + // is under one too. + JSScope* parentScope = instance->parentScope() ? instance->parentScope() : globalObject->moduleEnvironmentParentScope(); + if (instance->parentScope() && !(globalObject->isModuleScopeOverlay(parentScope) && globalObject->primaryModuleScopeOverlay())) { + throwTypeError(globalObject, scope, "A module graph instance's parent scope must be a module scope overlay of a global object whose modules are under one"_s); + return nullptr; + } + JSModuleEnvironment* env = JSModuleEnvironment::create(vm, globalObject, parentScope, symbolTable, jsTDZValue(), this); + RETURN_IF_EXCEPTION(scope, nullptr); + env->setGraphInstance(vm, instance); + // Register before recursing so import cycles terminate (status Linked), and + // in `created` so a failure further on can roll every record of this + // instantiation back out of the instance (Link() step 4.a). + instance->add(vm, this, env); + created.append(this); + + for (const auto& request : requestedModules()) { + AbstractModuleRecord* imported = hostResolveImportedModule(globalObject, request.m_specifier, request.type()); + RETURN_IF_EXCEPTION(scope, nullptr); + if (auto* importedSource = dynamicDowncast(imported)) { + importedSource->createInstanceEnvironment(globalObject, instance, created); + RETURN_IF_EXCEPTION(scope, nullptr); + } else if (is(imported)) { + // Every Cyclic Module Record an instance reaches must have its own + // state in the instance (the evaluation algorithm never falls back to + // the primary graph's); only Source Text Module Records can today. + throwTypeError(globalObject, scope, makeString("Module '"_s, imported->moduleKey().string(), "' cannot be instantiated into a module graph instance (only JavaScript and synthetic modules can)"_s)); + return nullptr; + } else { + // Synthetic records with per-instance state get an environment in + // the instance; others are shared with the primary graph. + imported->graphInstanceEnvironment(globalObject, instance, true); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + + // 7.c. Namespace imports bind the exporter's namespace in this instance; + // single imports that resolve to a namespace (export * as ns from) get that + // namespace materialised in its module's instance environment. + for (const auto& [key, in] : importEntries()) { + AbstractModuleRecord* importedModule = hostResolveImportedModule(globalObject, in.moduleRequest, in.moduleRequestType); + RETURN_IF_EXCEPTION(scope, nullptr); + if (in.type == ImportEntryType::Namespace) { + JSModuleNamespaceObject* ns = importedModule->getModuleNamespace(globalObject, instance, in.phase); + RETURN_IF_EXCEPTION(scope, nullptr); + bool putResult = false; + symbolTablePutTouchWatchpointSet(env, globalObject, in.localName, ns, false, true, putResult); + RETURN_IF_EXCEPTION(scope, nullptr); + continue; + } + Resolution resolution = importedModule->resolveExport(globalObject, in.importName); + RETURN_IF_EXCEPTION(scope, nullptr); + if (resolution.type == Resolution::Type::Resolved && resolution.localName == vm.propertyNames->starNamespacePrivateName) { + resolution.moduleRecord->getModuleNamespace(globalObject, instance); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + + // 21. var declarations start as undefined (lexical ones stay in TDZ). + UnlinkedModuleProgramCodeBlock* unlinkedCodeBlock = executable->unlinkedCodeBlock(); + for (const auto& variable : unlinkedCodeBlock->variableDeclarations()) { + SymbolTableEntry::Fast entry = symbolTable->get(variable.key.get()); + if (!entry.varOffset().isStack()) { + bool putResult = false; + symbolTablePutTouchWatchpointSet(env, globalObject, Identifier::fromUid(vm, variable.key.get()), jsUndefined(), false, true, putResult); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + + // 24. Function declarations: new function objects over the executables the + // primary instantiation linked, closed over this environment. + for (size_t i = 0, count = unlinkedCodeBlock->numberOfFunctionDecls(); i < count; ++i) { + FunctionExecutable* functionExecutable = i < m_functionDeclExecutables.size() ? m_functionDeclExecutables[i].get() : nullptr; + if (!functionExecutable) + continue; + SourceParseMode parseMode = functionExecutable->parseMode(); + JSFunction* function = nullptr; + if (isAsyncGeneratorWrapperParseMode(parseMode)) + function = JSAsyncGeneratorFunction::create(vm, globalObject, functionExecutable, env); + else if (isGeneratorWrapperParseMode(parseMode)) + function = JSGeneratorFunction::create(vm, globalObject, functionExecutable, env); + else if (isAsyncFunctionWrapperParseMode(parseMode)) + function = JSAsyncFunction::create(vm, globalObject, functionExecutable, env); + else + function = JSFunction::create(vm, globalObject, functionExecutable, env); + RETURN_IF_EXCEPTION(scope, nullptr); + bool putResult = false; + symbolTablePutTouchWatchpointSet(env, globalObject, unlinkedCodeBlock->functionDecl(i)->name(), function, false, true, putResult); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // import.meta: a fresh object per instance, carrying the instance for the host. + if (m_features & ImportMetaFeature) { + JSObject* meta = globalObject->moduleLoader()->createImportMetaProperties(globalObject, identifierToJSValue(vm, moduleKey()), this, nullptr); + RETURN_IF_EXCEPTION(scope, nullptr); + meta->putDirect(vm, vm.propertyNames->builtinNames().moduleGraphInstancePrivateName(), instance, static_cast(PropertyAttribute::DontEnum)); + bool putResult = false; + symbolTablePutTouchWatchpointSet(env, globalObject, vm.propertyNames->builtinNames().metaPrivateName(), meta, false, true, putResult); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + return env; +} + +// Link() step 4.a for an instance: an instantiation that failed part-way leaves +// no source-text environment behind, so a retry starts clean instead of +// evaluating half-initialised environments. Synthetic per-instance environments +// created on the way are complete on creation (their bindings are set when the +// environment is made) and stay in the instance, as an import() of that module +// alone would have left them. +static void rollBackInstantiation(ModuleGraphInstance* instance, const Vector& created) +{ + for (JSModuleRecord* record : created) + instance->remove(record); +} + +static void fillGraphInstanceImportSlots(JSGlobalObject* globalObject, const Vector& created, ModuleGraphInstance* instance) +{ + for (JSModuleRecord* record : created) { + if (JSModuleEnvironment* environment = instance->environment(record)) + environment->fillImportSlots(globalObject); + } +} + +JSModuleEnvironment* JSModuleRecord::instantiateIntoGraphInstance(JSGlobalObject* globalObject, ModuleGraphInstance* instance, ModulePhase phase) +{ + ModuleGraphInstance::BusyScope busy(globalObject, instance); + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + Vector created; + JSModuleEnvironment* env = createInstanceEnvironment(globalObject, instance, created); + if (scope.exception()) [[unlikely]] { + rollBackInstantiation(instance, created); + return nullptr; + } + fillGraphInstanceImportSlots(globalObject, created, instance); + if (phase == ModulePhase::Defer) { + // import defer: only the asynchronous transitive dependencies evaluate + // now; that must complete synchronously here. + OrderedHashSet asyncDependencies; + UncheckedKeyHashSet seen; + gatherAsynchronousTransitiveDependencies(asyncDependencies, seen, instance); + for (AbstractModuleRecord* dependency : asyncDependencies) { + auto* cyclic = dynamicDowncast(dependency); + if (!cyclic) + continue; +#if USE(BUN_JSC_ADDITIONS) + JSPromise* promise = cyclic->evaluate(globalObject, -1, nullptr, instance); +#else + JSPromise* promise = cyclic->evaluate(globalObject, instance); +#endif + RETURN_IF_EXCEPTION(scope, nullptr); + switch (promise->status()) { + case JSPromise::Status::Fulfilled: + continue; + case JSPromise::Status::Rejected: + promise->markAsHandled(); + scope.throwException(globalObject, promise->result()); + return nullptr; + case JSPromise::Status::Pending: + throwTypeError(globalObject, scope, makeString("Module '"_s, cyclic->moduleKey().string(), "' uses top-level await and cannot be evaluated synchronously"_s)); + return nullptr; + } + } + return env; + } +#if USE(BUN_JSC_ADDITIONS) + JSPromise* promise = CyclicModuleRecord::evaluate(globalObject, -1, nullptr, instance); +#else + JSPromise* promise = CyclicModuleRecord::evaluate(globalObject, instance); +#endif + RETURN_IF_EXCEPTION(scope, nullptr); + switch (promise->status()) { + case JSPromise::Status::Fulfilled: + return env; + case JSPromise::Status::Rejected: + promise->markAsHandled(); + scope.throwException(globalObject, promise->result()); + return nullptr; + case JSPromise::Status::Pending: + // Top-level await somewhere in the sub-graph: a synchronous caller + // cannot wait for it (the asynchronous form can). + throwTypeError(globalObject, scope, makeString("Module '"_s, moduleKey().string(), "' or one of its dependencies uses top-level await and cannot be evaluated synchronously"_s)); + return nullptr; + } + return env; +} + +JSPromise* JSModuleRecord::instantiateIntoGraphInstanceAsync(JSGlobalObject* globalObject, ModuleGraphInstance* instance, ModulePhase phase, JSPromise* dynamicImportPromise) +{ + ModuleGraphInstance::BusyScope busy(globalObject, instance); + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + Vector created; + createInstanceEnvironment(globalObject, instance, created); + if (scope.exception()) [[unlikely]] { + rollBackInstantiation(instance, created); + JSPromise* rejected = JSPromise::create(vm, globalObject->promiseStructure()); + rejected->rejectWithCaughtException(vm, scope); + return rejected; + } + fillGraphInstanceImportSlots(globalObject, created, instance); + if (phase == ModulePhase::Defer) { + // import defer: evaluate the asynchronous transitive dependencies now + // and wait for all of them; the rest runs on first namespace access. + OrderedHashSet asyncDependencies; + UncheckedKeyHashSet seen; + gatherAsynchronousTransitiveDependencies(asyncDependencies, seen, instance); + MarkedArgumentBuffer promises; + for (AbstractModuleRecord* dependency : asyncDependencies) { + auto* cyclic = dynamicDowncast(dependency); + if (!cyclic) + continue; +#if USE(BUN_JSC_ADDITIONS) + JSPromise* promise = cyclic->evaluate(globalObject, -1, dynamicImportPromise, instance); +#else + JSPromise* promise = cyclic->evaluate(globalObject, instance); +#endif + if (scope.exception()) [[unlikely]] { + JSPromise* rejected = JSPromise::create(vm, globalObject->promiseStructure()); + rejected->rejectWithCaughtException(vm, scope); + return rejected; + } + promises.append(promise); + } + if (promises.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + JSPromise* rejected = JSPromise::create(vm, globalObject->promiseStructure()); + rejected->rejectWithCaughtException(vm, scope); + return rejected; + } + // SafePerformPromiseAll: an AND-join through internal microtasks (first + // rejection rejects). Nothing here is script-observable or Strong<>-rooted. + JSPromise* result = JSPromise::create(vm, globalObject->promiseStructure()); + if (promises.isEmpty()) { + result->resolve(globalObject, vm, jsUndefined()); + RELEASE_AND_RETURN(scope, result); + } + auto* joinContext = JSPromiseCombinatorsGlobalContext::create(vm, result, jsUndefined(), promises.size()); + for (unsigned i = 0; i < promises.size(); ++i) + uncheckedDowncast(promises.at(i))->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleGraphInstanceDependencySettled, result, joinContext); + return result; + } +#if USE(BUN_JSC_ADDITIONS) + JSPromise* promise = CyclicModuleRecord::evaluate(globalObject, -1, dynamicImportPromise, instance); +#else + UNUSED_PARAM(dynamicImportPromise); + JSPromise* promise = CyclicModuleRecord::evaluate(globalObject, instance); +#endif + if (scope.exception()) [[unlikely]] { + JSPromise* rejected = JSPromise::create(vm, globalObject->promiseStructure()); + rejected->rejectWithCaughtException(vm, scope); + return rejected; + } + return promise; +} + +// ExecuteModule for a module graph instance: the record's body against its +// environment in the instance, with the instance's own execution state for a +// body with top-level await. +void JSModuleRecord::executeInstance(JSGlobalObject* globalObject, ModuleRecordInstance* recordInstance, JSPromise* capability) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!hasTLA()) { + ASSERT(!capability); + vm.interpreter.executeModuleProgram(this, recordInstance, m_retainedExecutable.get(), globalObject, recordInstance->environment(), jsUndefined(), jsNumber(static_cast(ResumeMode::NormalMode))); + pinRetainedCodeBlock(vm); + RETURN_IF_EXCEPTION(scope, void()); + return; + } + ASSERT(capability); + recordInstance->setAsyncCapability(vm, capability); + JSValue result = evaluateInstance(globalObject, recordInstance, jsUndefined(), jsNumber(static_cast(ResumeMode::NormalMode))); + asyncModuleResolveEvaluation(globalObject, vm, scope, recordInstance, result); +} + +// One step of a top-level-await module body in an instance (first run or a +// resumption after an await). +JSValue JSModuleRecord::evaluateInstance(JSGlobalObject* globalObject, ModuleRecordInstance* recordInstance, JSValue sentValue, JSValue resumeMode) +{ + VM& vm = globalObject->vm(); + JSValue result = vm.interpreter.executeModuleProgram(this, recordInstance, m_retainedExecutable.get(), globalObject, recordInstance->environment(), sentValue, resumeMode); + pinRetainedCodeBlock(vm); + return result; +} + +void JSModuleRecord::pinRetainedCodeBlock(VM& vm) +{ + if (m_retainedCodeBlock || !m_retainedExecutable) + return; + if (ModuleProgramCodeBlock* codeBlock = m_retainedExecutable->codeBlock()) + m_retainedCodeBlock.set(vm, this, codeBlock); +} + ModuleProgramExecutable* JSModuleRecord::getOrMakeExecutable(JSGlobalObject* globalObject) { ModuleProgramExecutable* executable = m_moduleProgramExecutable.get(); diff --git a/Source/JavaScriptCore/runtime/JSModuleRecord.h b/Source/JavaScriptCore/runtime/JSModuleRecord.h index 6dc3030056bb..6a2155651b93 100644 --- a/Source/JavaScriptCore/runtime/JSModuleRecord.h +++ b/Source/JavaScriptCore/runtime/JSModuleRecord.h @@ -32,10 +32,19 @@ namespace JSC { +class ModuleGraphInstance; class ModuleProgramExecutable; +class ModuleRecordInstance; // Based on the Source Text Module Record // http://www.ecma-international.org/ecma-262/6.0/#sec-source-text-module-records +class JSMap; +class FunctionExecutable; +class JSPromise; +class InternalFieldTuple; +class CodeBlock; + + class JSModuleRecord final : public CyclicModuleRecord { friend class LLIntOffsetsExtractor; public: @@ -71,13 +80,45 @@ class JSModuleRecord final : public CyclicModuleRecord { ModuleProgramExecutable* getOrMakeExecutable(JSGlobalObject*); + // Module graph instances. Instantiate this module and, recursively, every + // source text module it depends on, a further time into `instance`, reusing + // this record's ModuleProgramExecutable, CodeBlock and function executables, + // then evaluate the instance with the module evaluation algorithm against + // the instance's state (CyclicModuleRecord::evaluate(..., instance)). + // The synchronous form requires the evaluation to complete synchronously + // (throws for top-level await) and returns this module's environment in the + // instance; the asynchronous form returns the evaluation promise. + JS_EXPORT_PRIVATE JSModuleEnvironment* instantiateIntoGraphInstance(JSGlobalObject*, ModuleGraphInstance*, ModulePhase = ModulePhase::Evaluation); + // `dynamicImportPromise` is the import() promise this evaluation settles, + // when there is one, for the top-level-await deadlock check in + // innerModuleEvaluation (BUN_JSC_ADDITIONS); null otherwise. + JS_EXPORT_PRIVATE JSPromise* instantiateIntoGraphInstanceAsync(JSGlobalObject*, ModuleGraphInstance*, ModulePhase = ModulePhase::Evaluation, JSPromise* dynamicImportPromise = nullptr); + // ExecuteModule against this record's environment in `instance`. + void executeInstance(JSGlobalObject*, ModuleRecordInstance*, JSPromise* capability); + JSValue evaluateInstance(JSGlobalObject*, ModuleRecordInstance*, JSValue sentValue, JSValue resumeMode); + ModuleProgramExecutable* retainedExecutable() const { return m_retainedExecutable.get(); } + void pinRetainedCodeBlock(VM&); + // Once a record may be instantiated again it must keep its executable + // (normally dropped after evaluation) and its top-level function executables. + void retainForGraphInstances(VM&, ModuleProgramExecutable*, Vector>&&); + private: JSModuleRecord(VM&, Structure*, const Identifier&, const SourceCode&, CodeFeatures); void finishCreation(JSGlobalObject*, VM&); + JSModuleEnvironment* createInstanceEnvironment(JSGlobalObject*, ModuleGraphInstance*, Vector& created); + SourceCode m_sourceCode; WriteBarrier m_moduleProgramExecutable; + // Kept for graph instances: the executable past evaluation, and the linked + // executables of top-level function declarations (index = functionDecl(i)). + WriteBarrier m_retainedExecutable; + // The program CodeBlock whose constants own the nested FunctionExecutables + // every instance shares; held strongly so old-age jettison cannot re-link it + // (which would mint fresh executables for later instances). + WriteBarrier m_retainedCodeBlock; + Vector> m_functionDeclExecutables; CodeFeatures m_features; }; diff --git a/Source/JavaScriptCore/runtime/JSScope.cpp b/Source/JavaScriptCore/runtime/JSScope.cpp index 2963f80fa70e..3b2767c7ba84 100644 --- a/Source/JavaScriptCore/runtime/JSScope.cpp +++ b/Source/JavaScriptCore/runtime/JSScope.cpp @@ -97,6 +97,8 @@ static inline bool abstractAccess(JSGlobalObject* globalObject, JSScope* scope, SymbolTableEntry& entry = iter->value; ASSERT(!entry.isNull()); op = ResolveOp(makeType(ModuleVar, needsVarInjectionChecks), depth, nullptr, importedEnvironment, entry.watchpointSet(), entry.scopeOffset().offset(), resolution.localName.impl()); + if (auto slotIndex = moduleRecord->importSlotIndexFor(importedRecord)) + op.moduleImportSlot = 1 + JSModuleEnvironment::importSlotScopeOffset(moduleEnvironment->symbolTable(), *slotIndex).offset(); return true; } } diff --git a/Source/JavaScriptCore/runtime/Microtask.h b/Source/JavaScriptCore/runtime/Microtask.h index 73b5f8d3114c..86bd4d44d232 100644 --- a/Source/JavaScriptCore/runtime/Microtask.h +++ b/Source/JavaScriptCore/runtime/Microtask.h @@ -82,6 +82,9 @@ enum class InternalMicrotask : uint8_t { DynamicImportEvaluateSettled, DynamicImportDeferLoadSettled, DynamicImportDeferDependencySettled, + ModuleGraphInstanceLoadSettled, + ModuleGraphInstanceEvaluateSettled, + ModuleGraphInstanceDependencySettled, ImportModuleNamespace, #if ENABLE(WEBASSEMBLY) WebAssemblyCompileStreaming, diff --git a/Source/JavaScriptCore/runtime/ModuleGraphInstance.cpp b/Source/JavaScriptCore/runtime/ModuleGraphInstance.cpp new file mode 100644 index 000000000000..d6924bf29385 --- /dev/null +++ b/Source/JavaScriptCore/runtime/ModuleGraphInstance.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2026 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 "ModuleGraphInstance.h" + +#include "DeferTermination.h" +#include "Error.h" +#include "FrameTracers.h" +#include "JSCInlines.h" +#include "JSInternalFieldObjectImplInlines.h" +#include "JSModuleEnvironment.h" +#include "JSModuleNamespaceObject.h" +#include "JSPromise.h" +#include "ModuleGraphInstanceInlines.h" + +namespace JSC { + +const ClassInfo ModuleRecordInstance::s_info = { "ModuleRecordInstance"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleRecordInstance) }; +const ClassInfo ModuleGraphInstance::s_info = { "ModuleGraphInstance"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleGraphInstance) }; + +ModuleRecordInstance::ModuleRecordInstance(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void ModuleRecordInstance::destroy(JSCell* cell) +{ + SUPPRESS_MEMORY_UNSAFE_CAST auto* thisObject = static_cast(cell); + thisObject->~ModuleRecordInstance(); +} + +ModuleRecordInstance* ModuleRecordInstance::create(VM& vm, ModuleGraphInstance* graphInstance, AbstractModuleRecord* record, JSModuleEnvironment* environment) +{ + Structure* structure = vm.moduleRecordInstanceStructure.get(); + ModuleRecordInstance* instance = new (NotNull, allocateCell(vm)) ModuleRecordInstance(vm, structure); + instance->finishCreation(vm, graphInstance, record, environment); + return instance; +} + +void ModuleRecordInstance::finishCreation(VM& vm, ModuleGraphInstance* graphInstance, AbstractModuleRecord* record, JSModuleEnvironment* environment) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + auto initialValues = AbstractModuleRecord::initialValues(); + for (unsigned index = 0; index < numberOfInternalFields; ++index) + internalField(static_cast(index)).set(vm, this, initialValues[index]); + m_graphInstance.set(vm, this, graphInstance); + m_record.set(vm, this, record); + m_environment.setMayBeNull(vm, this, environment); +} + +template +void ModuleRecordInstance::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_record); + visitor.append(thisObject->m_graphInstance); + visitor.append(thisObject->m_environment); + visitor.append(thisObject->m_evaluationError); + visitor.append(thisObject->m_cycleRoot); + visitor.append(thisObject->m_topLevelCapability); + visitor.append(thisObject->m_asyncCapability); + visitor.append(thisObject->m_deferredNamespaceObject); + Locker locker { thisObject->cellLock() }; + visitor.append(thisObject->m_asyncParentModules.begin(), thisObject->m_asyncParentModules.end()); +} + +DEFINE_VISIT_CHILDREN(ModuleRecordInstance); + +void ModuleRecordInstance::appendAsyncParentModule(VM& vm, AbstractModuleRecord* record) +{ + Locker locker { cellLock() }; + m_asyncParentModules.append(WriteBarrier(vm, this, record)); +} + +// Same encoding as JSModuleRecord::isTopLevelExecutionFinished(): Field::State is +// the module body generator's resume point — a body that ran to completion leaves +// it at Executing (nothing to resume), a body suspended at a top-level await +// stores its resume label instead. +bool ModuleRecordInstance::isTopLevelExecutionFinished() const +{ + JSValue state = internalField(Field::State).get(); + return !state.isNumber() || state.asInt32AsAnyInt() == std::to_underlying(AbstractModuleRecord::State::Executing); +} + +void ModuleRecordInstance::setDeferredNamespaceObject(VM& vm, JSModuleNamespaceObject* namespaceObject) +{ + m_deferredNamespaceObject.setMayBeNull(vm, this, namespaceObject); +} + +ModuleGraphInstance::ModuleGraphInstance(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +ModuleGraphInstance* ModuleGraphInstance::create(VM& vm, JSGlobalObject* globalObject, JSScope* parentScope) +{ + Structure* structure = globalObject->moduleGraphInstanceStructure(); + ModuleGraphInstance* instance = new (NotNull, allocateCell(vm)) ModuleGraphInstance(vm, structure); + instance->finishCreation(vm, parentScope); + return instance; +} + +void ModuleGraphInstance::finishCreation(VM& vm, JSScope* parentScope) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + m_parentScope.setMayBeNull(vm, this, parentScope); +} + +template +void ModuleGraphInstance::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_parentScope); + Locker locker { thisObject->cellLock() }; + for (auto& [record, instance] : thisObject->m_records) + visitor.append(instance); +} + +DEFINE_VISIT_CHILDREN(ModuleGraphInstance); + +ModuleRecordInstance* ModuleGraphInstance::recordInstance(AbstractModuleRecord* record) const +{ + auto iterator = m_records.find(record); + return iterator == m_records.end() ? nullptr : iterator->value.get(); +} + +ModuleRecordInstance* ModuleGraphInstance::add(VM& vm, AbstractModuleRecord* record, JSModuleEnvironment* environment) +{ + RELEASE_ASSERT(!m_cleared); // callers check isCleared() and throw first + // Idempotent: host code that runs while a record is instantiated (a + // synthetic module's generator) may have instantiated it re-entrantly. + if (ModuleRecordInstance* existing = recordInstance(record)) + return existing; + ModuleRecordInstance* instance = ModuleRecordInstance::create(vm, this, record, environment); + environment->setGraphInstance(vm, this); + Locker locker { cellLock() }; + m_records.add(record, WriteBarrier(vm, this, instance)); + return instance; +} + +bool ModuleGraphInstance::remove(AbstractModuleRecord* record) +{ + Locker locker { cellLock() }; + return m_records.remove(record); +} + +void ModuleGraphInstance::destroy(JSCell* cell) +{ + SUPPRESS_MEMORY_UNSAFE_CAST auto* thisObject = static_cast(cell); + thisObject->~ModuleGraphInstance(); +} + +void ModuleGraphInstance::clear(JSGlobalObject* globalObject) +{ + if (m_busy) { + // An evaluation step is running against this instance (its module code + // asked for the disposal): finish that step coherently, clear after it. + m_clearPending = true; + return; + } + m_clearPending = false; + VM& vm = globalObject->vm(); + // Pending top-level evaluation promises are rejected below; keep them alive + // (they were reachable only through the records' state) across the + // allocation of the error. + MarkedArgumentBuffer pending; + { + Locker locker { cellLock() }; + m_cleared = true; + for (auto& entry : m_records) { + JSPromise* capability = entry.value->topLevelCapability(); + if (capability && capability->status() == JSPromise::Status::Pending) + pending.append(capability); + } + m_records.clear(); + } + RELEASE_ASSERT(!pending.hasOverflowed()); + if (pending.isEmpty()) + return; + // May run as a deferred clear when an evaluation step unwinds with an + // exception pending (BusyScope): rejecting is bookkeeping, not a new throw. + DeferTerminationForAWhile deferTermination(vm); + SuspendExceptionScope suspendException(vm); + JSObject* error = createTypeError(globalObject, "Module graph instance was disposed during evaluation"_s); + for (unsigned i = 0; i < pending.size(); ++i) + uncheckedDowncast(pending.at(i))->reject(vm, JSValue(error)); +} + +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/ModuleGraphInstance.h b/Source/JavaScriptCore/runtime/ModuleGraphInstance.h new file mode 100644 index 000000000000..c4408a7e0ed9 --- /dev/null +++ b/Source/JavaScriptCore/runtime/ModuleGraphInstance.h @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2026 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. + */ + +#pragma once + +#include "AbstractModuleRecord.h" +#include "CyclicModuleRecord.h" +#include "JSInternalFieldObjectImpl.h" +#include "JSDestructibleObject.h" +#include + +namespace JSC { + +class JSModuleEnvironment; +class JSModuleNamespaceObject; +class JSPromise; +class JSScope; +class ModuleGraphInstance; + +// The state of one module record within one ModuleGraphInstance: its module +// environment there and the record's evaluation state for that instance (the +// fields the module evaluation algorithm keeps on a Cyclic Module Record). Also +// the context object of that instance's asynchronous evaluation steps. +// It is also the generator object of the instance's module body (internal +// fields State and Frame, as on AbstractModuleRecord) and therefore the driver +// a top-level `for await` in that body resumes. +class ModuleRecordInstance final : public JSInternalFieldObjectImpl<2> { +public: + using Base = JSInternalFieldObjectImpl<2>; + static constexpr unsigned StructureFlags = Base::StructureFlags | StructureIsImmortal; + using Field = AbstractModuleRecord::Field; + static_assert(numberOfInternalFields == AbstractModuleRecord::numberOfInternalFields); + WriteBarrier& internalField(Field field) { return Base::internalField(static_cast(field)); } + const WriteBarrier& internalField(Field field) const { return Base::internalField(static_cast(field)); } + static constexpr DestructionMode needsDestruction = NeedsDestruction; + static void destroy(JSCell*); + + DECLARE_EXPORT_INFO; + DECLARE_VISIT_CHILDREN; + + template + static GCClient::IsoSubspace* subspaceFor(VM& vm) + { + return vm.moduleRecordInstanceSpace(); + } + + inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); + static ModuleRecordInstance* create(VM&, ModuleGraphInstance*, AbstractModuleRecord*, JSModuleEnvironment*); + + AbstractModuleRecord* record() const { return m_record.get(); } + ModuleGraphInstance* graphInstance() const { return m_graphInstance.get(); } + JSModuleEnvironment* environment() const { return m_environment.get(); } + + CyclicModuleRecord::Status status() const { return m_status; } + void setStatus(CyclicModuleRecord::Status status) { m_status = status; } + JSValue evaluationError() const { return m_evaluationError.get(); } + void setEvaluationError(VM& vm, JSValue error) { m_evaluationError.set(vm, this, error); } + unsigned dfsAncestorIndex() const { return m_dfsAncestorIndex; } + void setDFSAncestorIndex(unsigned index) { m_dfsAncestorIndex = index; } + CyclicModuleRecord* cycleRoot() const { return m_cycleRoot.get(); } + void setCycleRoot(VM& vm, CyclicModuleRecord* root) { m_cycleRoot.setMayBeNull(vm, this, root); } + AbstractModuleRecord::AsyncEvaluationOrder asyncEvaluationOrder() const { return m_asyncEvaluationOrder; } + void setAsyncEvaluationOrder(AbstractModuleRecord::AsyncEvaluationOrder order) { m_asyncEvaluationOrder = order; } + std::optional pendingAsyncDependencies() const { return m_pendingAsyncDependencies; } + void setPendingAsyncDependencies(std::optional value) { m_pendingAsyncDependencies = value; } + const Vector>& asyncParentModules() const LIFETIME_BOUND { return m_asyncParentModules; } + void appendAsyncParentModule(VM&, AbstractModuleRecord*); + JSPromise* topLevelCapability() const { return m_topLevelCapability.get(); } + void setTopLevelCapability(VM& vm, JSPromise* capability) { m_topLevelCapability.setMayBeNull(vm, this, capability); } + JSPromise* asyncCapability() const { return m_asyncCapability.get(); } + void setAsyncCapability(VM& vm, JSPromise* capability) { m_asyncCapability.setMayBeNull(vm, this, capability); } + // Generator state of a module body with top-level await (Field::State). + bool isTopLevelExecutionFinished() const; + JSModuleNamespaceObject* deferredNamespaceObject() const { return m_deferredNamespaceObject.get(); } + void setDeferredNamespaceObject(VM&, JSModuleNamespaceObject*); + +private: + ModuleRecordInstance(VM&, Structure*); + void finishCreation(VM&, ModuleGraphInstance*, AbstractModuleRecord*, JSModuleEnvironment*); + + WriteBarrier m_record; + WriteBarrier m_graphInstance; + WriteBarrier m_environment; + WriteBarrier m_evaluationError; + WriteBarrier m_cycleRoot; + WriteBarrier m_topLevelCapability; + WriteBarrier m_asyncCapability; + WriteBarrier m_deferredNamespaceObject; + Vector> m_asyncParentModules; + AbstractModuleRecord::AsyncEvaluationOrder m_asyncEvaluationOrder { }; + std::optional m_pendingAsyncDependencies; + unsigned m_dfsAncestorIndex { 0 }; + CyclicModuleRecord::Status m_status { CyclicModuleRecord::Status::Linked }; +}; + +// One instantiation of a module graph in a global object beyond the primary +// one: maps each module record instantiated for it to its ModuleRecordInstance +// (environment + evaluation state). Records not in the map are shared with the +// primary graph (their own environment and state apply). +class ModuleGraphInstance final : public JSDestructibleObject { +public: + using Base = JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + DECLARE_EXPORT_INFO; + DECLARE_VISIT_CHILDREN; + static constexpr DestructionMode needsDestruction = NeedsDestruction; + static void destroy(JSCell*); + + template + static GCClient::IsoSubspace* subspaceFor(VM& vm) + { + return vm.moduleGraphInstanceSpace(); + } + + inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); + JS_EXPORT_PRIVATE static ModuleGraphInstance* create(VM&, JSGlobalObject*, JSScope* parentScope); + + // The scope module environments of this instance are created under: an + // embedder-provided scope (e.g. a scope overlay) or the global object's + // module environment parent scope. + JSScope* parentScope() const { return m_parentScope.get(); } + void setParentScope(VM& vm, JSScope* scope) { m_parentScope.setMayBeNull(vm, this, scope); } + + ModuleRecordInstance* recordInstance(AbstractModuleRecord*) const; + JSModuleEnvironment* environment(AbstractModuleRecord* record) const + { + ModuleRecordInstance* instance = recordInstance(record); + return instance ? instance->environment() : nullptr; + } + ModuleRecordInstance* add(VM&, AbstractModuleRecord*, JSModuleEnvironment*); + bool remove(AbstractModuleRecord*); + // Drops every record's environment and state (the embedder is done with the + // instance; code of the instance that still runs keeps what it closes over). + // Releases every record's state; pending top-level evaluation promises of + // this instance are rejected and later asynchronous completions of its + // modules are dropped. + JS_EXPORT_PRIVATE void clear(JSGlobalObject*); + bool isCleared() const { return m_cleared; } + // Disposed by its owner: cleared, or to be cleared as soon as the evaluation + // step currently running against it returns (see BusyScope). + bool isDisposed() const { return m_cleared || m_clearPending; } + + // Brackets an evaluation step that runs against this instance (Evaluate(), + // an asynchronous completion or resumption); a clear() requested meanwhile + // is performed when the outermost step returns. + class BusyScope { + WTF_MAKE_NONCOPYABLE(BusyScope); + public: + BusyScope(JSGlobalObject* globalObject, ModuleGraphInstance* instance) + : m_globalObject(globalObject), m_instance(instance) + { + if (m_instance) + ++m_instance->m_busy; + } + ~BusyScope() + { + if (m_instance && !--m_instance->m_busy && m_instance->m_clearPending) + m_instance->clear(m_globalObject); + } + private: + JSGlobalObject* m_globalObject; + ModuleGraphInstance* m_instance; + }; + template void forEachRecord(const Functor&) const; + +private: + ModuleGraphInstance(VM&, Structure*); + void finishCreation(VM&, JSScope* parentScope); + + WriteBarrier m_parentScope; + UncheckedKeyHashMap> m_records; + bool m_cleared { false }; + bool m_clearPending { false }; + unsigned m_busy { 0 }; +}; + +template +void ModuleGraphInstance::forEachRecord(const Functor& functor) const +{ + for (auto& [record, instance] : m_records) + functor(*record, *instance.get()); +} + +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/ModuleGraphInstanceInlines.h b/Source/JavaScriptCore/runtime/ModuleGraphInstanceInlines.h new file mode 100644 index 000000000000..6fcf7fb2b2d0 --- /dev/null +++ b/Source/JavaScriptCore/runtime/ModuleGraphInstanceInlines.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 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. + */ + +#pragma once + +#include "ModuleGraphInstance.h" +#include "StructureCreateInlines.h" + +namespace JSC { + +Structure* ModuleRecordInstance::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +Structure* ModuleGraphInstance::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index 4577a58f42b6..a8b74ec85251 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -25,6 +25,7 @@ #include "config.h" #include "ModuleRegistryEntry.h" +#include "SyntheticModuleRecord.h" #include "JSCInlines.h" #include "JSModuleLoader.h" @@ -149,10 +150,23 @@ JSPromise* ModuleRegistryEntry::loadPromise() const return m_loadPromise.get(); } -JSValue ModuleRegistryEntry::error(JSGlobalObject* globalObject) const +JSValue ModuleRegistryEntry::error(JSGlobalObject* globalObject, IncludeEvaluationError includeEvaluationError) const { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + if (includeEvaluationError == IncludeEvaluationError::No && m_status == Status::EvaluationFailed) { + // Only the record's own evaluation having thrown is excluded; a load + // failure of a dependency recorded on this entry still counts. + auto* cyclic = dynamicDowncast(m_record.get()); + if (cyclic && cyclic->status() == CyclicModuleRecord::Status::Evaluated && cyclic->evaluationError()) + return { }; + // A synthetic record with per-instance state is regenerated in each + // instance, so the primary's evaluation failure is not the instance's + // either; one shared with the primary graph keeps it. + auto* synthetic = dynamicDowncast(m_record.get()); + if (synthetic && synthetic->hasPerGraphInstanceState()) + return { }; + } if (JSValue error = m_error.get()) { if (m_status == Status::FetchFailed) { if (auto* errorInstance = dynamicDowncast(error)) @@ -160,7 +174,7 @@ JSValue ModuleRegistryEntry::error(JSGlobalObject* globalObject) const } RELEASE_AND_RETURN(scope, error); } - if (m_record) { + if (m_record && includeEvaluationError == IncludeEvaluationError::Yes) { if (auto* cyclic = dynamicDowncast(m_record.get())) RELEASE_AND_RETURN(scope, cyclic->evaluationError()); } @@ -179,6 +193,16 @@ auto ModuleRegistryEntry::status() const -> Status return m_status; } +bool ModuleRegistryEntry::hasSettledFailure() const +{ + if (m_status == Status::FetchFailed || m_status == Status::InstantiationFailed) + return true; + if (m_record) + return false; + auto rejected = [](JSPromise* promise) { return promise && promise->status() == JSPromise::Status::Rejected; }; + return rejected(m_fetchPromise.get()) || rejected(m_modulePromise.get()) || rejected(m_loadPromise.get()); +} + void ModuleRegistryEntry::setRecord(VM& vm, AbstractModuleRecord* record) { m_record.set(vm, this, record); diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h index 79db0ce32115..f0dd365bd53f 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h @@ -73,9 +73,16 @@ class ModuleRegistryEntry final : public JSCell { JSPromise* ensureFetchPromise(JSGlobalObject*); JSPromise* ensureModulePromise(JSGlobalObject*); JSPromise* loadPromise() const; - JSValue error(JSGlobalObject*) const; + // The error this entry settled with: a fetch, instantiation or dependency + // load error stored on the entry, else (unless excluded) the record's + // evaluation error. + enum class IncludeEvaluationError : bool { No, Yes }; + JSValue error(JSGlobalObject*, IncludeEvaluationError = IncludeEvaluationError::Yes) const; JSValue fetchError() const; Status status() const; + // A finished, failed load: fetch or record creation / instantiation failed and + // nothing is in flight (an entry another program run may retry from scratch). + bool hasSettledFailure() const; void setRecord(VM&, AbstractModuleRecord*); void setLoadPromise(VM&, JSPromise*); diff --git a/Source/JavaScriptCore/runtime/OptionsList.h b/Source/JavaScriptCore/runtime/OptionsList.h index 00b28122ead7..681fce9546be 100644 --- a/Source/JavaScriptCore/runtime/OptionsList.h +++ b/Source/JavaScriptCore/runtime/OptionsList.h @@ -602,6 +602,7 @@ bool hasCapacityToUseLargeGigacage(); v(Bool, dumpModuleRecord, false, Normal, nullptr) \ v(Bool, dumpModuleLoadingState, false, Normal, nullptr) \ v(Bool, exposeInternalModuleLoader, false, Normal, "expose the internal module loader object to the global space for debugging"_s) \ + v(Bool, useModuleGraphInstances, false, Normal, "Allow a linked module graph to be instantiated more than once per global object (ModuleGraphInstance)"_s) \ \ v(Bool, exposePrivateIdentifiers, false, Normal, "Allow non-builtin scripts to use private identifiers. Mostly useful to expose @superSamplerBegin/End intrinsics for profiling"_s) \ \ diff --git a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp index 79fec402d118..c27c7b1068bf 100644 --- a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp @@ -25,6 +25,11 @@ #include "config.h" #include "SyntheticModuleRecord.h" +#include "SourceProvider.h" +#include "StructureInlines.h" +#include "ArrayConstructor.h" +#include "ObjectConstructor.h" +#include "JSArray.h" #include "ArgList.h" #include "BuiltinNames.h" @@ -85,8 +90,14 @@ Synchronousness SyntheticModuleRecord::link(JSGlobalObject*, RefPtrvm(); + auto scope = DECLARE_THROW_SCOPE(vm); + // A record first produced for a module graph instance gives the primary its + // own values when the primary graph evaluates it. + materializePrimaryIfPending(globalObject); + RETURN_IF_EXCEPTION(scope, { }); return jsUndefined(); } @@ -174,6 +185,9 @@ void SyntheticModuleRecord::materializeLazyExport(JSGlobalObject* globalObject, if (localName == vm.propertyNames->starNamespacePrivateName) return; + if (m_primaryPending) + RELEASE_AND_RETURN(scope, materializePrimaryIfPending(globalObject)); + JSModuleEnvironment* environment = moduleEnvironment(); SymbolTable* symbolTable = environment->symbolTable(); ScopeOffset scopeOffset; @@ -236,7 +250,105 @@ SyntheticModuleRecord* SyntheticModuleRecord::parseJSONModule(JSGlobalObject* gl JSValue result = JSONParseWithException(globalObject, sourceCode.view()); RETURN_IF_EXCEPTION(scope, { }); - RELEASE_AND_RETURN(scope, SyntheticModuleRecord::tryCreateDefaultExportSyntheticModule(globalObject, moduleKey, result, SourceProviderSourceType::JSON)); + SyntheticModuleRecord* record = SyntheticModuleRecord::tryCreateDefaultExportSyntheticModule(globalObject, moduleKey, result, SourceProviderSourceType::JSON); + RETURN_IF_EXCEPTION(scope, { }); + if (record && Options::useModuleGraphInstances()) + record->m_jsonSource = WTF::move(sourceCode); + RELEASE_AND_RETURN(scope, record); +} + +void SyntheticModuleRecord::materializePrimaryIfPending(JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + // While a graph is loading, the primary is nobody's business yet. + if (!m_primaryPending || !m_provider || globalObject->currentGraphInstanceForLoading()) + return; + m_primaryPending = false; // before generate(): the provider may re-enter + MarkedArgumentBuffer values; + Vector names; + m_provider->generate(globalObject, moduleKey(), names, values); + if (scope.exception()) [[unlikely]] { + m_primaryPending = true; // a later use retries + return; + } + if (values.hasOverflowed()) [[unlikely]] { + m_primaryPending = true; + throwOutOfMemoryError(globalObject, scope); + return; + } + JSModuleEnvironment* environment = moduleEnvironment(); + SymbolTable* symbolTable = environment->symbolTable(); + for (const auto& [key, entry] : exportEntries()) { + SymbolTableEntry::Fast symbolEntry = symbolTable->get(entry.localName.impl()); + if (symbolEntry.isNull()) + continue; + JSValue value = jsUndefined(); + for (unsigned i = 0; i < names.size(); ++i) { + if (names[i] == entry.localName) { + value = values.at(i); + break; + } + } + environment->variableAt(symbolEntry.scopeOffset()).set(vm, environment, value ? value : jsUndefined()); + } +} + +bool SyntheticModuleRecord::hasPerGraphInstanceState() const +{ + // Two explicit signals: a JSON module re-parses its source per instance, and + // a host provider that says so regenerates its values per instance. Every + // other synthetic module is shared with the primary graph. + if (!m_jsonSource.isNull()) + return true; + return m_provider && m_provider->regeneratesPerGraphInstance(); +} + +JSModuleEnvironment* SyntheticModuleRecord::createGraphInstanceEnvironment(JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(hasPerGraphInstanceState()); + JSModuleEnvironment* primary = moduleEnvironment(); + JSModuleEnvironment* environment = JSModuleEnvironment::create(vm, globalObject, nullptr, primary->symbolTable(), jsTDZValue(), this); + RETURN_IF_EXCEPTION(scope, nullptr); + if (!m_jsonSource.isNull()) { + JSValue value = JSONParseWithException(globalObject, m_jsonSource.view()); + RETURN_IF_EXCEPTION(scope, nullptr); + bool putResult = false; + symbolTablePutTouchWatchpointSet(environment, globalObject, vm.propertyNames->defaultKeyword, value, false, true, putResult); + RETURN_IF_EXCEPTION(scope, nullptr); + return environment; + } + if (m_provider && m_provider->regeneratesPerGraphInstance()) { + // The host produces this graph's values (the caller has set the graph as + // the current loading instance); names not produced stay undefined. + MarkedArgumentBuffer values; + Vector names; + m_provider->generate(globalObject, moduleKey(), names, values); + RETURN_IF_EXCEPTION(scope, nullptr); + if (values.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + SymbolTable* symbolTable = primary->symbolTable(); + for (const auto& [key, entry] : exportEntries()) { + SymbolTableEntry::Fast symbolEntry = symbolTable->get(entry.localName.impl()); + if (symbolEntry.isNull()) + continue; + JSValue value = jsUndefined(); + for (unsigned i = 0; i < names.size(); ++i) { + if (names[i] == entry.localName) { + value = values.at(i); + break; + } + } + environment->variableAt(symbolEntry.scopeOffset()).set(vm, environment, value ? value : jsUndefined()); + } + return environment; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; } SyntheticModuleRecord* SyntheticModuleRecord::createTextModule(JSGlobalObject* globalObject, const Identifier& moduleKey, SourceCode&& sourceCode) diff --git a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.h b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.h index beede61817b9..ef1c11139afe 100644 --- a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.h +++ b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.h @@ -31,6 +31,8 @@ namespace JSC { +class SyntheticSourceProvider; + class JSGlobalObject; // https://tc39.es/proposal-json-modules/#sec-synthetic-module-records @@ -56,6 +58,22 @@ class SyntheticModuleRecord final : public AbstractModuleRecord { static SyntheticModuleRecord* create(JSGlobalObject*, VM&, Structure*, const Identifier& moduleKey, SourceProviderSourceType); static SyntheticModuleRecord* parseJSONModule(JSGlobalObject*, const Identifier& moduleKey, SourceCode&&); + // Module graph instances: JSON modules carry mutable state (their parsed + // value), so each graph gets its own environment with a fresh parse. + // True for data modules: a JSON source to re-parse, or exports that are all + // plain data (primitives / arrays / plain objects), deep-copied per graph. + // Native/builtin modules (functions, host objects, lazy exports) are shared. + bool hasPerGraphInstanceState() const; + JSModuleEnvironment* createGraphInstanceEnvironment(JSGlobalObject*); + // Host synthetic modules with per-graph state of their own (a CommonJS + // module behind an ESM import): the provider regenerates per graph, and if + // the record was first created for a graph, the primary bindings stay lazy + // until the primary graph links to them (then the provider runs for it). + void setSyntheticSourceProvider(RefPtr&& provider, bool primaryPending) { m_provider = WTF::move(provider); m_primaryPending = primaryPending; } + // The primary environment's values are produced on first use by the primary graph. + bool primaryPending() const { return m_primaryPending; } + SyntheticSourceProvider* syntheticSourceProvider() const { return m_provider.get(); } + void materializePrimaryIfPending(JSGlobalObject*); static SyntheticModuleRecord* createTextModule(JSGlobalObject*, const Identifier& moduleKey, SourceCode&&); Synchronousness link(JSGlobalObject*, RefPtr = nullptr); @@ -93,6 +111,11 @@ class SyntheticModuleRecord final : public AbstractModuleRecord { #if USE(BUN_JSC_ADDITIONS) WriteBarrier m_lazyExportsSource; #endif + // Module graph instances: how to produce this record's bindings again for + // another instance (JSON source to re-parse, or the host's provider). + SourceCode m_jsonSource; + RefPtr m_provider; + bool m_primaryPending { false }; }; } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index 9252010d8989..35a14da65361 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -103,6 +103,7 @@ #include "ModuleLoaderPayloadInlines.h" #include "ModuleProgramCodeBlockInlines.h" #include "ModuleProgramExecutableInlines.h" +#include "ModuleGraphInstanceInlines.h" #include "ModuleRegistryEntryInlines.h" #include "NarrowingNumberPredictionFuzzerAgent.h" #include "NativeExecutable.h" @@ -365,6 +366,7 @@ VM::VM(VMType vmType, HeapType heapType, WTF::RunLoop* runLoop, bool* success) moduleLoaderStructure.setWithoutWriteBarrier(JSModuleLoader::createStructure(*this, nullptr, jsNull())); moduleRegistryEntryStructure.setWithoutWriteBarrier(ModuleRegistryEntry::createStructure(*this, nullptr, jsNull())); moduleLoadingContextStructure.setWithoutWriteBarrier(ModuleLoadingContext::createStructure(*this, nullptr, jsNull())); + moduleRecordInstanceStructure.setWithoutWriteBarrier(ModuleRecordInstance::createStructure(*this, nullptr, jsNull())); moduleLoaderPayloadStructure.setWithoutWriteBarrier(ModuleLoaderPayload::createStructure(*this, nullptr, jsNull())); moduleGraphLoadingStateStructure.setWithoutWriteBarrier(ModuleGraphLoadingState::createStructure(*this, nullptr, jsNull())); promiseCombinatorsContextStructure.setWithoutWriteBarrier(JSPromiseCombinatorsContext::createStructure(*this, nullptr, jsNull())); @@ -2043,6 +2045,7 @@ void VM::visitAggregateImpl(Visitor& visitor) visitor.append(moduleLoaderStructure); visitor.append(moduleRegistryEntryStructure); visitor.append(moduleLoadingContextStructure); + visitor.append(moduleRecordInstanceStructure); visitor.append(moduleLoaderPayloadStructure); visitor.append(moduleGraphLoadingStateStructure); visitor.append(promiseCombinatorsContextStructure); diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 98f2371b7036..42bb16568e70 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -558,6 +558,7 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { WriteBarrier moduleLoaderStructure; WriteBarrier moduleRegistryEntryStructure; WriteBarrier moduleLoadingContextStructure; + WriteBarrier moduleRecordInstanceStructure; WriteBarrier moduleLoaderPayloadStructure; WriteBarrier moduleGraphLoadingStateStructure; WriteBarrier promiseCombinatorsContextStructure; diff --git a/Source/JavaScriptCore/tools/JSDollarVM.cpp b/Source/JavaScriptCore/tools/JSDollarVM.cpp index 3eb5f9a6c8e1..3abd81c1012c 100644 --- a/Source/JavaScriptCore/tools/JSDollarVM.cpp +++ b/Source/JavaScriptCore/tools/JSDollarVM.cpp @@ -55,6 +55,11 @@ #include "JSCInlines.h" #include "JSGlobalProxyInlines.h" #include "JSONObject.h" +#include "JSMapInlines.h" +#include "JSModuleEnvironment.h" +#include "JSModuleRecord.h" +#include "ModuleGraphInstance.h" +#include "JSModuleNamespaceObject.h" #include "JSPromise.h" #include "JSString.h" #include "LinkBuffer.h" @@ -2242,6 +2247,7 @@ static JSC_DECLARE_HOST_FUNCTION(functionEnableDebuggerModeWhenIdle); static JSC_DECLARE_HOST_FUNCTION(functionDisableDebuggerModeWhenIdle); static JSC_DECLARE_HOST_FUNCTION(functionDeleteAllCodeWhenIdle); static JSC_DECLARE_HOST_FUNCTION(functionGlobalObjectCount); +static JSC_DECLARE_HOST_FUNCTION(functionInstantiateModuleGraph); static JSC_DECLARE_HOST_FUNCTION(functionGlobalObjectForObject); static JSC_DECLARE_HOST_FUNCTION(functionGetGetterSetter); static JSC_DECLARE_HOST_FUNCTION(functionLoadGetterFromGetterSetter); @@ -3915,6 +3921,43 @@ JSC_DEFINE_HOST_FUNCTION(functionDeleteAllCodeWhenIdle, (JSGlobalObject* globalO return JSValue::encode(jsUndefined()); } +// $vm.instantiateModuleGraph(moduleNamespaceObject[, instance]) +// Instantiates (and evaluates) the namespace's module and its source text +// dependencies a further time into `instance` (a ModuleGraphInstance; a new one +// if omitted) and returns { namespace, environment, instance }: the module's +// namespace object and environment in that instance. +JSC_DEFINE_HOST_FUNCTION(functionInstantiateModuleGraph, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + DollarVMAssertScope assertScope; + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!Options::useModuleGraphInstances()) + return throwVMTypeError(globalObject, scope, "useModuleGraphInstances is disabled"_s); + auto* ns = dynamicDowncast(callFrame->argument(0)); + if (!ns) + return throwVMTypeError(globalObject, scope, "expected a module namespace object"_s); + auto* record = dynamicDowncast(ns->moduleRecord()); + if (!record) + return throwVMTypeError(globalObject, scope, "namespace does not belong to a source text module"_s); + JSValue instanceValue = callFrame->argument(1); + auto* instance = dynamicDowncast(instanceValue); + if (!instance) { + if (!instanceValue.isUndefined()) + return throwVMTypeError(globalObject, scope, "expected a ModuleGraphInstance"_s); + instance = ModuleGraphInstance::create(vm, globalObject, nullptr); + } + JSModuleEnvironment* environment = record->instantiateIntoGraphInstance(globalObject, instance); + RETURN_IF_EXCEPTION(scope, {}); + JSModuleNamespaceObject* namespaceObject = record->getModuleNamespace(globalObject, instance); + RETURN_IF_EXCEPTION(scope, {}); + JSObject* result = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + result->putDirect(vm, Identifier::fromString(vm, "namespace"_s), namespaceObject); + result->putDirect(vm, Identifier::fromString(vm, "environment"_s), environment); + result->putDirect(vm, Identifier::fromString(vm, "instance"_s), instance); + return JSValue::encode(result); +} + JSC_DEFINE_HOST_FUNCTION(functionGlobalObjectCount, (JSGlobalObject* globalObject, CallFrame*)) { DollarVMAssertScope assertScope; @@ -5614,6 +5657,7 @@ void JSDollarVM::finishCreation(VM& vm) addFunction(vm, alwaysAllow, "deleteAllCodeWhenIdle"_s, functionDeleteAllCodeWhenIdle, 0); addFunction(vm, allowIfNotFuzz, "globalObjectCount"_s, functionGlobalObjectCount, 0); + addFunction(vm, allowIfNotFuzz, "instantiateModuleGraph"_s, functionInstantiateModuleGraph, 2); addFunction(vm, allowIfNotFuzz, "globalObjectForObject"_s, functionGlobalObjectForObject, 1); addFunction(vm, allowIfNotFuzz, "getGetterSetter"_s, functionGetGetterSetter, 2);