diff --git a/JSTests/stress/resolve-and-get-from-scope.js b/JSTests/stress/resolve-and-get-from-scope.js new file mode 100644 index 000000000000..d768145f8f1d --- /dev/null +++ b/JSTests/stress/resolve-and-get-from-scope.js @@ -0,0 +1,141 @@ +// resolve_scope + get_from_scope fused into resolve_and_get_from_scope: the cases that differ in how they resolve. + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`bad value: ${String(actual)}, expected ${String(expected)}`); +} +function shouldThrow(func, errorType, message) { + let error; + try { + func(); + } catch (e) { + error = e; + } + if (!(error instanceof errorType)) + throw new Error(`expected ${errorType.name}, got ${String(error)}`); + if (message !== undefined && error.message !== message) + throw new Error(`bad message: ${error.message}`); +} + +// GlobalVar, GlobalLexicalVar, GlobalProperty. +var globalVar = 1; +let globalLet = 2; +globalThis.globalProp = 3; +function readGlobals() { return globalVar + globalLet + globalProp; } +for (let i = 0; i < 1e4; ++i) + shouldBe(readGlobals(), 6); +globalVar = 10; +globalLet = 20; +globalThis.globalProp = 30; +shouldBe(readGlobals(), 60); + +// A global that does not exist yet: UnresolvedProperty, then GlobalProperty once defined. +function readLate() { return lateGlobal; } +shouldThrow(readLate, ReferenceError); +globalThis.lateGlobal = 7; +for (let i = 0; i < 1e4; ++i) + shouldBe(readLate(), 7); +delete globalThis.lateGlobal; +shouldThrow(readLate, ReferenceError); + +// typeof on an undeclared name must not throw. +function typeofUndeclared() { return typeof neverDeclaredAnywhere; } +for (let i = 0; i < 1e4; ++i) + shouldBe(typeofUndeclared(), "undefined"); + +// A global lexical in its TDZ. +function readTDZ() { return tdzLet; } +shouldThrow(readTDZ, ReferenceError); +let tdzLet = 5; +shouldBe(readTDZ(), 5); + +// ClosureVar through several scope levels. +function outer() { + let a = 1; + return function middle() { + let b = 2; + return function inner() { + return a + b; + }; + }(); +} +{ + const inner = outer(); + for (let i = 0; i < 1e4; ++i) + shouldBe(inner(), 3); +} + +// A bare call resolved through a scope passes undefined as `this`: sloppy callee sees globalThis, strict sees undefined. +function sloppyThis() { return this; } +function strictThis() { "use strict"; return this; } +function callThem() { return [sloppyThis(), strictThis()]; } +for (let i = 0; i < 1e4; ++i) { + const [sloppy, strict] = callThem(); + shouldBe(sloppy, globalThis); + shouldBe(strict, undefined); +} +{ + let closureSloppy = function () { return this; }; + let closureStrict = function () { "use strict"; return this; }; + function callClosures() { return [closureSloppy(), closureStrict()]; } + for (let i = 0; i < 1e4; ++i) { + const [sloppy, strict] = callClosures(); + shouldBe(sloppy, globalThis); + shouldBe(strict, undefined); + } +} + +// Tagged templates resolved through a scope likewise. +function tag(strings) { return [this, strings[0]]; } +function callTag() { return tag`x`; } +for (let i = 0; i < 1e4; ++i) { + const [thisValue, str] = callTag(); + shouldBe(thisValue, globalThis); + shouldBe(str, "x"); +} + +// Inside `with`, resolution is dynamic and stays unfused: the with object wins and is `this` for calls. +function withRead(obj) { + with (obj) + return [globalVar, f()]; +} +{ + const obj = { globalVar: 99, f() { return this; } }; + for (let i = 0; i < 1e4; ++i) { + const [value, thisValue] = withRead(obj); + shouldBe(value, 99); + shouldBe(thisValue, obj); + } + const [value, thisValue] = withRead({ f() { return this; } }); + shouldBe(value, 10); +} + +// Sloppy direct eval injecting a var flips the *WithVarInjectionChecks types. +function injected() { + eval("var injectedVar = 1"); + function read() { return injectedVar; } + for (let i = 0; i < 1e4; ++i) + shouldBe(read(), 1); + eval("var injectedVar = 2"); + shouldBe(read(), 2); +} +injected(); + +// A read of a global whose lexical binding epoch changes after caching. +function readShadowed() { return shadowedLater; } +globalThis.shadowedLater = "prop"; +for (let i = 0; i < 1e4; ++i) + shouldBe(readShadowed(), "prop"); + +// A function nested inside `with` resolves through the with object at runtime even though its own scope chain is static. +{ + const obj = { h() { return this; }, nestedX: 1 }; + function makeNested() { with (obj) { return function nested() { return [h(), nestedX, typeof nestedX]; }; } } + const nested = makeNested(); + for (let i = 0; i < 1e4; ++i) { + const [thisValue, value, type] = nested(); + shouldBe(thisValue, obj); + shouldBe(value, 1); + shouldBe(type, "number"); + } +} diff --git a/Source/JavaScriptCore/bytecode/BytecodeList.rb b/Source/JavaScriptCore/bytecode/BytecodeList.rb index 08ad777dbbd9..c076fcd29a1a 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeList.rb +++ b/Source/JavaScriptCore/bytecode/BytecodeList.rb @@ -562,6 +562,44 @@ operand: :offset, } +# resolve_scope followed by get_from_scope on the resolved scope, as one instruction. `scope` is the base scope the +# resolve starts from; the resolved scope never touches a register. The metadata is the two ops' metadata laid out +# back to back (resolve first), which the baseline thunks rely on. +op :resolve_and_get_from_scope, + args: { + dst: VirtualRegister, + scope: VirtualRegister, + var: unsigned, + getPutInfo: GetPutInfo, + localScopeDepth: unsigned, + offset: unsigned, + valueProfile: unsigned, + }, + metadata: { + resolveType: ResolveType, + _0: { + localScopeDepth: unsigned, + globalLexicalBindingEpoch: unsigned, + }, + _1: { + lexicalEnvironment: WriteBarrierBase[JSCell], + symbolTable: WriteBarrierBase[SymbolTable], + constantScope: WriteBarrierBase[JSScope], + globalLexicalEnvironment: WriteBarrierBase[JSGlobalLexicalEnvironment], + globalObject: WriteBarrierBase[JSGlobalObject], + }, + getPutInfo: GetPutInfo, + _2: { + watchpointSet: InlineWatchpointSet.*, + structureID: WriteBarrierStructureID, + }, + operand: uintptr_t, + }, + metadata_initializers: { + getPutInfo: :getPutInfo, + operand: :offset, + } + op :put_to_scope, args: { scope: VirtualRegister, diff --git a/Source/JavaScriptCore/bytecode/BytecodeUseDef.cpp b/Source/JavaScriptCore/bytecode/BytecodeUseDef.cpp index 256f127a094d..a5cf84893174 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeUseDef.cpp +++ b/Source/JavaScriptCore/bytecode/BytecodeUseDef.cpp @@ -177,6 +177,7 @@ void computeUsesForBytecodeIndexImpl(const JSInstruction* instruction, Checkpoin USES(OpResolveScope, scope) USES(OpResolveScopeForHoistingFuncDeclInEval, scope) USES(OpGetFromScope, scope) + USES(OpResolveAndGetFromScope, scope) USES(OpToPrimitive, src) USES(OpToPropertyKey, src) USES(OpToPropertyKeyOrNumber, src) @@ -521,6 +522,7 @@ void computeDefsForBytecodeIndexImpl(unsigned numVars, const JSInstruction* inst } DEFS(OpGetFromScope, dst) + DEFS(OpResolveAndGetFromScope, dst) DEFS(OpCall, dst) DEFS(OpTailCall, dst) DEFS(OpCallDirectEval, dst) diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index d8915876a702..ee733cf3f917 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -629,6 +629,44 @@ bool CodeBlock::finishCreation(VM& vm, ScriptExecutable* ownerExecutable, Unlink break; } + case op_resolve_and_get_from_scope: { + INITIALIZE_METADATA(OpResolveAndGetFromScope) + + metadata.m_watchpointSet = nullptr; + const Identifier& ident = identifier(bytecode.m_var); + ResolveType resolveType = bytecode.m_getPutInfo.resolveType(); + RELEASE_ASSERT(resolveType != ResolvedClosureVar); + ASSERT(!isInitialization(bytecode.m_getPutInfo.initializationMode())); + + ResolveOp op = JSScope::abstractResolve(m_globalObject.get(), bytecode.m_localScopeDepth, scope, ident, Get, resolveType, InitializationMode::NotInitialization); + + // The resolve half (see op_resolve_scope). + metadata.m_resolveType = op.type; + metadata.m_localScopeDepth = op.depth; + if (op.lexicalEnvironment) { + if (op.type == ModuleVar) { + if (stronglyReferencedModuleEnvironments.add(uncheckedDowncast(op.lexicalEnvironment)).isNewEntry) + addConstant(ConcurrentJSLocker(m_lock), op.lexicalEnvironment); + metadata.m_lexicalEnvironment.set(vm, this, op.lexicalEnvironment); + } else + metadata.m_symbolTable.set(vm, this, op.lexicalEnvironment->symbolTable()); + } else if (JSScope* constantScope = JSScope::constantScopeForCodeBlock(op.type, this)) { + metadata.m_constantScope.set(vm, this, constantScope); + if (op.type == GlobalProperty || op.type == GlobalPropertyWithVarInjectionChecks) + metadata.m_globalLexicalBindingEpoch = m_globalObject->globalLexicalBindingEpoch(); + } else + metadata.m_globalObject.clear(); + + // The get half (see op_get_from_scope). + metadata.m_getPutInfo = GetPutInfo(bytecode.m_getPutInfo.resolveMode(), op.type == ModuleVar ? ClosureVar : op.type, bytecode.m_getPutInfo.initializationMode(), bytecode.m_getPutInfo.ecmaMode()); + if (op.type == GlobalVar || op.type == GlobalVarWithVarInjectionChecks || op.type == GlobalLexicalVar || op.type == GlobalLexicalVarWithVarInjectionChecks) + metadata.m_watchpointSet = op.watchpointSet; + else if (op.structure) + metadata.m_structureID.set(vm, this, op.structure); + metadata.m_operand = op.operand; + break; + } + case op_put_to_scope: { INITIALIZE_METADATA(OpPutToScope) @@ -1746,6 +1784,14 @@ void CodeBlock::reconcileLLIntInlineCachesAtGCEnd() m_metadata->forEach(handleGetPutFromScope); m_metadata->forEach(handleGetPutFromScope); + m_metadata->forEach([&] (auto& metadata) { + WriteBarrierBase& symbolTable = metadata.m_symbolTable; + if (symbolTable && !vm.heap.isMarked(symbolTable.get())) { + dataLogLnIf(Options::verboseOSR(), "Clearing dead symbolTable ", RawPointer(symbolTable.get())); + symbolTable.clear(); + } + handleGetPutFromScope(metadata); + }); } // We can't just remove all the sets when we clear the caches since we might have created a watchpoint set @@ -3321,6 +3367,19 @@ void CodeBlock::notifyLexicalBindingUpdate() } break; } + case op_resolve_and_get_from_scope: { + auto bytecode = instruction->as(); + auto& metadata = bytecode.metadata(this); + ResolveType originalResolveType = metadata.m_resolveType; + if (originalResolveType == GlobalProperty || originalResolveType == GlobalPropertyWithVarInjectionChecks) { + const Identifier& ident = identifier(bytecode.m_var); + if (isShadowed(ident.impl())) + metadata.m_globalLexicalBindingEpoch = 0; + else + metadata.m_globalLexicalBindingEpoch = globalObject->globalLexicalBindingEpoch(); + } + break; + } default: break; } diff --git a/Source/JavaScriptCore/bytecode/Opcode.h b/Source/JavaScriptCore/bytecode/Opcode.h index 7959334f188f..7dda00875247 100644 --- a/Source/JavaScriptCore/bytecode/Opcode.h +++ b/Source/JavaScriptCore/bytecode/Opcode.h @@ -111,6 +111,7 @@ static constexpr unsigned bitWidthForMaxBytecodeStructLength = WTF::getMSBSet(ma macro(OpConstruct) \ macro(OpSuperConstruct) \ macro(OpGetFromScope) \ + macro(OpResolveAndGetFromScope) \ macro(OpGetPrivateName) \ macro(OpNewArrayWithSpecies) \ macro(OpAsyncIteratorNext) \ diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp index 18d6f049703a..f930e666ae50 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp @@ -2750,6 +2750,26 @@ RegisterID* BytecodeGenerator::emitGetFromScope(RegisterID* dst, RegisterID* sco RELEASE_ASSERT_NOT_REACHED(); } +RegisterID* BytecodeGenerator::emitResolveAndGetFromScope(RegisterID* dst, const Variable& variable, ResolveMode resolveMode) +{ + // A `with` scope resolves dynamically and may run observable traps; keep those as two instructions so an OSR exit + // between them never resolves twice. + if (!canFuseResolveAndGet(variable)) { + RefPtr scope = emitResolveScope(nullptr, variable); + return emitGetFromScope(dst, scope.get(), variable, resolveMode); + } + OpResolveAndGetFromScope::emit( + this, + kill(dst), + scopeRegister(), + addConstant(variable.ident()), + GetPutInfo(resolveMode, resolveType(), InitializationMode::NotInitialization, ecmaMode()), + localScopeDepth(), + 0, + nextValueProfileIndex()); + return dst; +} + RegisterID* BytecodeGenerator::emitPutToScope(RegisterID* scope, const Variable& variable, RegisterID* value, ResolveMode resolveMode, InitializationMode initializationMode) { switch (variable.offset().kind()) { diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h index d665dc390661..f8489c4f8aa1 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h @@ -918,6 +918,15 @@ namespace JSC { RegisterID* emitResolveConstantLocal(RegisterID* dst, const Variable&); RegisterID* emitResolveScope(RegisterID* dst, const Variable&); RegisterID* emitGetFromScope(RegisterID* dst, RegisterID* scope, const Variable&, ResolveMode); + // emitResolveScope + emitGetFromScope as one instruction when the resolve is a static non-local lookup. + RegisterID* emitResolveAndGetFromScope(RegisterID* dst, const Variable&, ResolveMode); + // A non-local lookup with no `with` scope anywhere on the chain (own or enclosing functions): the resolved + // scope is never a `with` object, so a call through it takes `this` as undefined, the same as to_this makes of + // the scope, and the resolve never runs a trap. + bool canFuseResolveAndGet(const Variable& variable) + { + return variable.offset().kind() == VarKind::Invalid && resolveType() != Dynamic && !(lexicallyScopedFeatures() & TaintedByWithScopeLexicallyScopedFeature); + } RegisterID* emitPutToScope(RegisterID* scope, const Variable&, RegisterID* value, ResolveMode, InitializationMode); RegisterID* emitPutToScopeDynamic(RegisterID* scope, const Identifier&, RegisterID* value, ResolveMode, InitializationMode); diff --git a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp index 27bed9d5e093..2088815f2585 100644 --- a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp +++ b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp @@ -294,13 +294,12 @@ RegisterID* ResolveNode::emitBytecode(BytecodeGenerator& generator, RegisterID* } generator.emitExpressionInfo(divot, m_start, divot); - RefPtr scope = generator.emitResolveScope(dst, var); RegisterID* finalDest = generator.finalDestination(dst); if (!generator.needsTDZCheck(var)) - generator.emitGetFromScope(finalDest, scope.get(), var, ThrowIfNotFound); + generator.emitResolveAndGetFromScope(finalDest, var, ThrowIfNotFound); else { RefPtr uncheckedResult = generator.newTemporary(); - generator.emitGetFromScope(uncheckedResult.get(), scope.get(), var, ThrowIfNotFound); + generator.emitResolveAndGetFromScope(uncheckedResult.get(), var, ThrowIfNotFound); generator.emitTDZCheck(uncheckedResult.get(), m_ident); generator.move(finalDest, uncheckedResult.get()); } @@ -384,8 +383,13 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis JSTextPosition newDivot = divotStart() + identifier.length(); generator.emitExpressionInfo(newDivot, divotStart(), newDivot); - generator.move(base.get(), generator.emitResolveScope(base.get(), var)); - generator.emitGetFromScope(tag.get(), base.get(), var, ThrowIfNotFound); + if (generator.canFuseResolveAndGet(var)) { + generator.emitLoad(base.get(), jsUndefined()); + generator.emitResolveAndGetFromScope(tag.get(), var, ThrowIfNotFound); + } else { + generator.move(base.get(), generator.emitResolveScope(base.get(), var)); + generator.emitGetFromScope(tag.get(), base.get(), var, ThrowIfNotFound); + } generator.emitTDZCheckIfNecessary(var, tag.get(), nullptr); } } else if (m_tag->isBracketAccessorNode()) { @@ -1309,10 +1313,15 @@ RegisterID* EvalFunctionCallNode::emitBytecode(BytecodeGenerator& generator, Reg else { JSTextPosition newDivot = divotStart() + 4; generator.emitExpressionInfo(newDivot, divotStart(), newDivot); - generator.move( - callArguments.thisRegister(), - generator.emitResolveScope(callArguments.thisRegister(), var)); - generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound); + if (generator.canFuseResolveAndGet(var)) { + generator.emitLoad(callArguments.thisRegister(), jsUndefined()); + generator.emitResolveAndGetFromScope(func.get(), var, ThrowIfNotFound); + } else { + generator.move( + callArguments.thisRegister(), + generator.emitResolveScope(callArguments.thisRegister(), var)); + generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound); + } generator.emitTDZCheckIfNecessary(var, func.get(), nullptr); } @@ -1454,10 +1463,15 @@ RegisterID* FunctionCallResolveNode::emitBytecode(BytecodeGenerator& generator, expectedFunction = NoExpectedFunction; } else { generator.emitExpressionInfo(newDivot, divotStart(), newDivot); - generator.move( - callArguments.thisRegister(), - generator.emitResolveScope(callArguments.thisRegister(), var)); - generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound); + if (generator.canFuseResolveAndGet(var)) { + generator.emitLoad(callArguments.thisRegister(), jsUndefined()); + generator.emitResolveAndGetFromScope(func.get(), var, ThrowIfNotFound); + } else { + generator.move( + callArguments.thisRegister(), + generator.emitResolveScope(callArguments.thisRegister(), var)); + generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound); + } generator.emitTDZCheckIfNecessary(var, func.get(), nullptr); } @@ -2829,13 +2843,12 @@ RegisterID* TypeOfResolveNode::emitBytecode(BytecodeGenerator& generator, Regist return generator.emitTypeOf(generator.finalDestination(dst), local); } - RefPtr scope = generator.emitResolveScope(dst, var); - RefPtr value = generator.emitGetFromScope(generator.newTemporary(), scope.get(), var, DoNotThrowIfNotFound); + RefPtr value = generator.emitResolveAndGetFromScope(generator.newTemporary(), var, DoNotThrowIfNotFound); generator.emitExpressionInfo(newDivot, newDivot, divotEnd()); generator.emitTDZCheckIfNecessary(var, value.get(), nullptr); if (dst == generator.ignoredResult()) return nullptr; - return generator.emitTypeOf(generator.finalDestination(dst, scope.get()), value.get()); + return generator.emitTypeOf(generator.finalDestination(dst), value.get()); } // ------------------------------ TypeOfValueNode ----------------------------------- diff --git a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp index b57e685f8bae..0e158f0cb6a0 100644 --- a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp +++ b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp @@ -1550,6 +1550,34 @@ class ByteCodeParser { bool needsDynamicLookup(ResolveType, OpcodeID); + // The resolve_scope and get_from_scope lowerings, shared with resolve_and_get_from_scope. + struct ResolveScopeInputs { + unsigned identifierNumber; + ResolveType resolveType; + unsigned depth; + JSScope* constantScope; + JSCell* lexicalEnvironment; + SymbolTable* symbolTable; + }; + enum class ResolveScopeKind : uint8_t { + Static, // a constant or a SkipScope chain: no side effects + Dynamic, // a ResolveScope node: the get must be a GetDynamicVar + Exited, // a ForceOSRExit was emitted: what follows in this bytecode is dead + }; + struct ResolvedScope { + Node* node; + ResolveScopeKind kind; + }; + ResolvedScope parseResolveScope(const ResolveScopeInputs&, Node* baseScope); + struct GetFromScopeInputs { + unsigned identifierNumber; + GetPutInfo getPutInfo; + Structure* structure; + InlineWatchpointSet* watchpoints; + uintptr_t operand; + }; + Node* parseGetFromScope(const GetFromScopeInputs&, Node* scope, Node* baseScope, bool forceDynamic); + void pruneUnreachableNodes(); VM* const m_vm; @@ -7036,6 +7064,241 @@ bool NODELETE ByteCodeParser::needsDynamicLookup(ResolveType type, OpcodeID opco return false; } +static uint64_t NODELETE makeDynamicVarOpInfo(unsigned identifierNumber, unsigned getPutInfo) +{ + static_assert(sizeof(identifierNumber) == 4, + "We cannot fit identifierNumber into the high bits of m_opInfo"); + return static_cast(identifierNumber) | (static_cast(getPutInfo) << 32); +} + +auto ByteCodeParser::parseResolveScope(const ResolveScopeInputs& inputs, Node* baseScope) -> ResolvedScope +{ + ResolveType resolveType = inputs.resolveType; + unsigned identifierNumber = inputs.identifierNumber; + + if (needsDynamicLookup(resolveType, op_resolve_scope)) + return { addToGraph(ResolveScope, OpInfo(identifierNumber), baseScope), ResolveScopeKind::Dynamic }; + + // get_from_scope and put_to_scope depend on this watchpoint forcing OSR exit, so they don't add their own watchpoints. + if (needsVarInjectionChecks(resolveType)) + m_graph.watchpoints().addLazily(m_inlineStackTop->m_codeBlock->globalObject()->varInjectionWatchpointSet()); + + ResolveScopeKind kind = ResolveScopeKind::Static; + if (resolveType == GlobalProperty || resolveType == GlobalPropertyWithVarInjectionChecks) { + JSGlobalObject* globalObject = m_inlineStackTop->m_codeBlock->globalObject(); + if (!m_graph.watchGlobalProperty(globalObject, identifierNumber)) { + addToGraph(ForceOSRExit); + kind = ResolveScopeKind::Exited; + } + } + + switch (resolveType) { + case GlobalProperty: + case GlobalVar: + case GlobalPropertyWithVarInjectionChecks: + case GlobalVarWithVarInjectionChecks: + case GlobalLexicalVar: + case GlobalLexicalVarWithVarInjectionChecks: { + RELEASE_ASSERT(inputs.constantScope); + RELEASE_ASSERT(inputs.constantScope == JSScope::constantScopeForCodeBlock(resolveType, m_inlineStackTop->m_codeBlock)); + addToGraph(Phantom, baseScope); + return { weakJSConstant(inputs.constantScope), kind }; + } + case ModuleVar: { + // Module environment is already strongly referenced by the CodeBlock. + // 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, baseScope); + return { weakJSConstant(inputs.lexicalEnvironment), kind }; + } + case ResolvedClosureVar: + case ClosureVar: + case ClosureVarWithVarInjectionChecks: { + Node* localBase = baseScope; + addToGraph(Phantom, localBase); // OSR exit cannot handle resolve_scope on a DCE'd scope. + + // We have various forms of constant folding here. This is necessary to avoid + // spurious recompiles in dead-but-foldable code. + + if (inputs.symbolTable) { + if (JSScope* scope = inputs.symbolTable->singleton().inferredValue()) { + m_graph.watchpoints().addLazily(m_graph, inputs.symbolTable); + return { weakJSConstant(scope), kind }; + } + } + if (JSScope* scope = localBase->dynamicCastConstant()) { + for (unsigned n = inputs.depth; n--;) + scope = scope->next(); + return { weakJSConstant(scope), kind }; + } + for (unsigned n = inputs.depth; n--;) + localBase = addToGraph(SkipScope, localBase); + return { localBase, kind }; + } + case UnresolvedProperty: + case UnresolvedPropertyWithVarInjectionChecks: { + addToGraph(Phantom, baseScope); + addToGraph(ForceOSRExit); + return { addToGraph(JSConstant, OpInfo(m_constantNull)), ResolveScopeKind::Exited }; + } + case Dynamic: + RELEASE_ASSERT_NOT_REACHED(); + break; + } + RELEASE_ASSERT_NOT_REACHED(); +} + +// `scope` is the node the get reads from; `baseScope` is the bytecode's scope operand, kept live for OSR exit. +Node* ByteCodeParser::parseGetFromScope(const GetFromScopeInputs& inputs, Node* scope, Node* baseScope, bool forceDynamic) +{ + unsigned identifierNumber = inputs.identifierNumber; + UniquedStringImpl* uid = m_graph.identifiers()[identifierNumber]; + GetPutInfo getPutInfo = inputs.getPutInfo; + ResolveType resolveType = getPutInfo.resolveType(); + + if (forceDynamic || needsDynamicLookup(resolveType, op_get_from_scope)) { + uint64_t opInfo1 = makeDynamicVarOpInfo(identifierNumber, getPutInfo.operand()); + SpeculatedType prediction = getPrediction(); + return addToGraph(GetDynamicVar, OpInfo(opInfo1), OpInfo(prediction), scope); + } + + JSGlobalObject* globalObject = m_inlineStackTop->m_codeBlock->globalObject(); + + switch (resolveType) { + case GlobalProperty: + case GlobalPropertyWithVarInjectionChecks: { + if (!m_graph.watchGlobalProperty(globalObject, identifierNumber)) + addToGraph(ForceOSRExit); + + SpeculatedType prediction = getPrediction(); + + CacheableIdentifier identifier = CacheableIdentifier::createFromIdentifierOwnedByCodeBlock(m_inlineStackTop->m_profiledBlock, uid); + + // op_get_from_scope for a global property should walk the + // proto chain of the global object searching for the desired property + GetByStatus::LookupMode lookupMode = GetByStatus::LookupMode::Normal; + GetByStatus status = GetByStatus::computeFor(m_inlineStackTop->m_profiledBlock, m_currentIndex, globalObject, inputs.structure, identifier, lookupMode); + + if (status.state() != GetByStatus::Simple + || status.numVariants() != 1 + || status[0].structureSet().size() != 1) { + auto* data = m_graph.m_getByIdData.add(GetByIdData { identifier, CacheType::GetByIdSelf }); + return addToGraph(GetByIdFlush, OpInfo(data), OpInfo(prediction), scope); + } + + Node* base = weakJSConstant(globalObject); + Node* result = load(prediction, base, base, identifierNumber, status[0]); + addToGraph(Phantom, baseScope); + return result; + } + case GlobalVar: + case GlobalVarWithVarInjectionChecks: + case GlobalLexicalVar: + case GlobalLexicalVarWithVarInjectionChecks: { + addToGraph(Phantom, baseScope); + InlineWatchpointSet* watchpoints = inputs.watchpoints; + if (watchpoints && watchpoints->state() == IsWatched) { + // This has a fun concurrency story. There is the possibility of a race in two + // directions: + // + // We see that the set IsWatched, but in the meantime it gets invalidated: this is + // fine because if we saw that it IsWatched then we add a watchpoint. If it gets + // invalidated, then this compilation is invalidated. Note that in the meantime we + // may load an absurd value from the global object. It's fine to load an absurd + // value if the compilation is invalidated anyway. + // + // We see that the set IsWatched, but the value isn't yet initialized: this isn't + // possible because of the ordering of operations. + // + // Here's how we order operations: + // + // Main thread stores to the global object: always store a value first, and only + // after that do we touch the watchpoint set. There is a fence in the touch, that + // ensures that the store to the global object always happens before the touch on the + // set. + // + // Compilation thread: always first load the state of the watchpoint set, and then + // load the value. The WatchpointSet::state() method does fences for us to ensure + // that the load of the state happens before our load of the value. + // + // Finalizing compilation: this happens on the main thread and synchronously checks + // validity of all watchpoint sets. + // + // We will only perform optimizations if the load of the state yields IsWatched. That + // means that at least one store would have happened to initialize the original value + // of the variable (that is, the value we'd like to constant fold to). There may be + // other stores that happen after that, but those stores will invalidate the + // watchpoint set and also the compilation. + + // Note that we need to use the operand, which is a direct pointer at the global, + // rather than looking up the global by doing variableAt(offset). That's because the + // internal data structures of JSSegmentedVariableObject are not thread-safe even + // though accessing the global itself is. The segmentation involves a vector spine + // that resizes with malloc/free, so if new globals unrelated to the one we are + // reading are added, we might access freed memory if we do variableAt(). + WriteBarrier* pointer = std::bit_cast*>(inputs.operand); + JSValue value = pointer->get(); + if (value) { + m_graph.watchpoints().addLazily(*watchpoints); + return weakJSConstant(value); + } + } + + SpeculatedType prediction = getPrediction(); + NodeType nodeType; + if (resolveType == GlobalVar || resolveType == GlobalVarWithVarInjectionChecks) + nodeType = GetGlobalVar; + else + nodeType = GetGlobalLexicalVariable; + Node* value = addToGraph(nodeType, OpInfo(inputs.operand), OpInfo(prediction)); + if (resolveType == GlobalLexicalVar || resolveType == GlobalLexicalVarWithVarInjectionChecks) + addToGraph(CheckNotEmpty, value); + return value; + } + case ResolvedClosureVar: + case ClosureVar: + case ClosureVarWithVarInjectionChecks: { + Node* scopeNode = scope; + + // Ideally we wouldn't have to do this Phantom. But: + // + // For the constant case: we must do it because otherwise we would have no way of knowing + // that the scope is live at OSR here. + // + // For the non-constant case: GetClosureVar could be DCE'd, but baseline's implementation + // won't be able to handle an Undefined scope. + addToGraph(Phantom, scopeNode); + + // Constant folding in the bytecode parser is important for performance. This may not + // have executed yet. If it hasn't, then we won't have a prediction. Lacking a + // prediction, we'd otherwise think that it has to exit. Then when it did execute, we + // would recompile. But if we can fold it here, we avoid the exit. + if (JSValue value = m_graph.tryGetConstantClosureVar(scopeNode, ScopeOffset(inputs.operand))) + return weakJSConstant(value); + + SpeculatedType prediction = SpecNone; + if (resolveType == ResolvedClosureVar) { + // ResolvedClosureVar is not used normally. It is very special internal ResolveType, mainly used for generators and private fields. + // In these variables, it can happen that we use JSEmpty as a result of op_get_from_scope (which becomes a TDZ error in normal ClosureVar). + // And this JSEmpty is still legit. The problem is that ValueProfile never tells about JSEmpty since it sees no value is stored when JSEmpty + // is stored. We workaround this very special internal use case by explicitly setting SpecEmpty when ValueProfile tells this is SpecNone. + prediction = getPredictionWithoutOSRExit(); + if (prediction == SpecNone) + prediction = SpecEmpty; + } else + prediction = getPrediction(); + return addToGraph(GetClosureVar, OpInfo(inputs.operand), OpInfo(prediction), scopeNode); + } + case UnresolvedProperty: + case UnresolvedPropertyWithVarInjectionChecks: + case ModuleVar: + case Dynamic: + RELEASE_ASSERT_NOT_REACHED(); + break; + } + RELEASE_ASSERT_NOT_REACHED(); +} + GetByOffsetMethod ByteCodeParser::planLoad(const ObjectPropertyCondition& condition) { VERBOSE_LOG("Planning a load: ", condition, "\n"); @@ -8196,13 +8459,6 @@ void ByteCodeParser::parseGetById(const JSInstruction* currentInstruction, unsig handleGetById(bytecode.m_dst, prediction, base, identifier, identifierNumber, getByStatus, type, nextOpcodeIndex()); } -static uint64_t NODELETE makeDynamicVarOpInfo(unsigned identifierNumber, unsigned getPutInfo) -{ - static_assert(sizeof(identifierNumber) == 4, - "We cannot fit identifierNumber into the high bits of m_opInfo"); - return static_cast(identifierNumber) | (static_cast(getPutInfo) << 32); -} - // The idiom: // if (true) { ...; goto label; } else label: continue // Allows using NEXT_OPCODE as a statement, even in unbraced if+else, while containing a `continue`. @@ -10409,113 +10665,34 @@ void ByteCodeParser::parseBlock(unsigned limit) auto bytecode = currentInstruction->as(); auto& metadata = bytecode.metadata(codeBlock); - ResolveType resolveType; - unsigned depth; - JSScope* constantScope = nullptr; - JSCell* lexicalEnvironment = nullptr; - SymbolTable* symbolTable = nullptr; + ResolveScopeInputs inputs { m_inlineStackTop->m_identifierRemap[bytecode.m_var], Dynamic, 0, nullptr, nullptr, nullptr }; { ConcurrentJSLocker locker(m_inlineStackTop->m_profiledBlock->m_lock); - resolveType = metadata.m_resolveType; - depth = metadata.m_localScopeDepth; - switch (resolveType) { + inputs.resolveType = metadata.m_resolveType; + inputs.depth = metadata.m_localScopeDepth; + switch (inputs.resolveType) { case GlobalProperty: case GlobalVar: case GlobalPropertyWithVarInjectionChecks: case GlobalVarWithVarInjectionChecks: case GlobalLexicalVar: case GlobalLexicalVarWithVarInjectionChecks: - constantScope = metadata.m_constantScope.get(); + inputs.constantScope = metadata.m_constantScope.get(); break; case ModuleVar: - lexicalEnvironment = metadata.m_lexicalEnvironment.get(); + inputs.lexicalEnvironment = metadata.m_lexicalEnvironment.get(); break; case ResolvedClosureVar: case ClosureVar: case ClosureVarWithVarInjectionChecks: - symbolTable = metadata.m_symbolTable.get(); + inputs.symbolTable = metadata.m_symbolTable.get(); break; default: break; } } - if (needsDynamicLookup(resolveType, op_resolve_scope)) { - unsigned identifierNumber = m_inlineStackTop->m_identifierRemap[bytecode.m_var]; - set(bytecode.m_dst, addToGraph(ResolveScope, OpInfo(identifierNumber), get(bytecode.m_scope))); - NEXT_OPCODE(op_resolve_scope); - } - - // get_from_scope and put_to_scope depend on this watchpoint forcing OSR exit, so they don't add their own watchpoints. - if (needsVarInjectionChecks(resolveType)) - m_graph.watchpoints().addLazily(m_inlineStackTop->m_codeBlock->globalObject()->varInjectionWatchpointSet()); - - if (resolveType == GlobalProperty || resolveType == GlobalPropertyWithVarInjectionChecks) { - JSGlobalObject* globalObject = m_inlineStackTop->m_codeBlock->globalObject(); - unsigned identifierNumber = m_inlineStackTop->m_identifierRemap[bytecode.m_var]; - if (!m_graph.watchGlobalProperty(globalObject, identifierNumber)) - addToGraph(ForceOSRExit); - } - - switch (resolveType) { - case GlobalProperty: - case GlobalVar: - case GlobalPropertyWithVarInjectionChecks: - case GlobalVarWithVarInjectionChecks: - case GlobalLexicalVar: - case GlobalLexicalVarWithVarInjectionChecks: { - RELEASE_ASSERT(constantScope); - RELEASE_ASSERT(constantScope == JSScope::constantScopeForCodeBlock(resolveType, m_inlineStackTop->m_codeBlock)); - set(bytecode.m_dst, weakJSConstant(constantScope)); - addToGraph(Phantom, get(bytecode.m_scope)); - 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)); - break; - } - case ResolvedClosureVar: - case ClosureVar: - case ClosureVarWithVarInjectionChecks: { - Node* localBase = get(bytecode.m_scope); - addToGraph(Phantom, localBase); // OSR exit cannot handle resolve_scope on a DCE'd scope. - - // We have various forms of constant folding here. This is necessary to avoid - // spurious recompiles in dead-but-foldable code. - - if (symbolTable) { - if (JSScope* scope = symbolTable->singleton().inferredValue()) { - m_graph.watchpoints().addLazily(m_graph, symbolTable); - set(bytecode.m_dst, weakJSConstant(scope)); - break; - } - } - if (JSScope* scope = localBase->dynamicCastConstant()) { - for (unsigned n = depth; n--;) - scope = scope->next(); - set(bytecode.m_dst, weakJSConstant(scope)); - break; - } - for (unsigned n = depth; n--;) - localBase = addToGraph(SkipScope, localBase); - set(bytecode.m_dst, localBase); - break; - } - case UnresolvedProperty: - case UnresolvedPropertyWithVarInjectionChecks: { - addToGraph(Phantom, get(bytecode.m_scope)); - addToGraph(ForceOSRExit); - set(bytecode.m_dst, addToGraph(JSConstant, OpInfo(m_constantNull))); - break; - } - case Dynamic: - RELEASE_ASSERT_NOT_REACHED(); - break; - } + set(bytecode.m_dst, parseResolveScope(inputs, get(bytecode.m_scope)).node); NEXT_OPCODE(op_resolve_scope); } case op_resolve_scope_for_hoisting_func_decl_in_eval: { @@ -10529,174 +10706,81 @@ void ByteCodeParser::parseBlock(unsigned limit) case op_get_from_scope: { auto bytecode = currentInstruction->as(); auto& metadata = bytecode.metadata(codeBlock); - unsigned identifierNumber = m_inlineStackTop->m_identifierRemap[bytecode.m_var]; - UniquedStringImpl* uid = m_graph.identifiers()[identifierNumber]; - ResolveType resolveType; - GetPutInfo getPutInfo(0); - Structure* structure = nullptr; - InlineWatchpointSet* watchpoints = nullptr; - uintptr_t operand; + GetFromScopeInputs inputs { m_inlineStackTop->m_identifierRemap[bytecode.m_var], GetPutInfo(0), nullptr, nullptr, 0 }; { ConcurrentJSLocker locker(m_inlineStackTop->m_profiledBlock->m_lock); - getPutInfo = metadata.m_getPutInfo; - resolveType = getPutInfo.resolveType(); + inputs.getPutInfo = metadata.m_getPutInfo; + ResolveType resolveType = inputs.getPutInfo.resolveType(); if (resolveType == GlobalVar || resolveType == GlobalVarWithVarInjectionChecks || resolveType == GlobalLexicalVar || resolveType == GlobalLexicalVarWithVarInjectionChecks) - watchpoints = metadata.m_watchpointSet; + inputs.watchpoints = metadata.m_watchpointSet; else if (resolveType == GlobalProperty || resolveType == GlobalPropertyWithVarInjectionChecks) - structure = metadata.m_structureID.get(); - operand = metadata.m_operand; - } - - if (needsDynamicLookup(resolveType, op_get_from_scope)) { - uint64_t opInfo1 = makeDynamicVarOpInfo(identifierNumber, getPutInfo.operand()); - SpeculatedType prediction = getPrediction(); - set(bytecode.m_dst, - addToGraph(GetDynamicVar, OpInfo(opInfo1), OpInfo(prediction), get(bytecode.m_scope))); - NEXT_OPCODE(op_get_from_scope); + inputs.structure = metadata.m_structureID.get(); + inputs.operand = metadata.m_operand; } - JSGlobalObject* globalObject = m_inlineStackTop->m_codeBlock->globalObject(); - - switch (resolveType) { - case GlobalProperty: - case GlobalPropertyWithVarInjectionChecks: { - if (!m_graph.watchGlobalProperty(globalObject, identifierNumber)) - addToGraph(ForceOSRExit); - - SpeculatedType prediction = getPrediction(); - - CacheableIdentifier identifier = CacheableIdentifier::createFromIdentifierOwnedByCodeBlock(m_inlineStackTop->m_profiledBlock, uid); + Node* scope = get(bytecode.m_scope); + set(bytecode.m_dst, parseGetFromScope(inputs, scope, scope, false)); + NEXT_OPCODE(op_get_from_scope); + } - // op_get_from_scope for a global property should walk the - // proto chain of the global object searching for the desired property - GetByStatus::LookupMode lookupMode = GetByStatus::LookupMode::Normal; - GetByStatus status = GetByStatus::computeFor(m_inlineStackTop->m_profiledBlock, m_currentIndex, globalObject, structure, identifier, lookupMode); + case op_resolve_and_get_from_scope: { + auto bytecode = currentInstruction->as(); + auto& metadata = bytecode.metadata(codeBlock); - if (status.state() != GetByStatus::Simple - || status.numVariants() != 1 - || status[0].structureSet().size() != 1) { - auto* data = m_graph.m_getByIdData.add(GetByIdData { identifier, CacheType::GetByIdSelf }); - set(bytecode.m_dst, addToGraph(GetByIdFlush, OpInfo(data), OpInfo(prediction), get(bytecode.m_scope))); + unsigned identifierNumber = m_inlineStackTop->m_identifierRemap[bytecode.m_var]; + ResolveScopeInputs resolveInputs { identifierNumber, Dynamic, 0, nullptr, nullptr, nullptr }; + GetFromScopeInputs getInputs { identifierNumber, GetPutInfo(0), nullptr, nullptr, 0 }; + { + ConcurrentJSLocker locker(m_inlineStackTop->m_profiledBlock->m_lock); + resolveInputs.resolveType = metadata.m_resolveType; + resolveInputs.depth = metadata.m_localScopeDepth; + switch (resolveInputs.resolveType) { + case GlobalProperty: + case GlobalVar: + case GlobalPropertyWithVarInjectionChecks: + case GlobalVarWithVarInjectionChecks: + case GlobalLexicalVar: + case GlobalLexicalVarWithVarInjectionChecks: + resolveInputs.constantScope = metadata.m_constantScope.get(); break; - } - - Node* base = weakJSConstant(globalObject); - Node* result = load(prediction, base, base, identifierNumber, status[0]); - addToGraph(Phantom, get(bytecode.m_scope)); - set(bytecode.m_dst, result); - break; - } - case GlobalVar: - case GlobalVarWithVarInjectionChecks: - case GlobalLexicalVar: - case GlobalLexicalVarWithVarInjectionChecks: { - addToGraph(Phantom, get(bytecode.m_scope)); - if (watchpoints && watchpoints->state() == IsWatched) { - // This has a fun concurrency story. There is the possibility of a race in two - // directions: - // - // We see that the set IsWatched, but in the meantime it gets invalidated: this is - // fine because if we saw that it IsWatched then we add a watchpoint. If it gets - // invalidated, then this compilation is invalidated. Note that in the meantime we - // may load an absurd value from the global object. It's fine to load an absurd - // value if the compilation is invalidated anyway. - // - // We see that the set IsWatched, but the value isn't yet initialized: this isn't - // possible because of the ordering of operations. - // - // Here's how we order operations: - // - // Main thread stores to the global object: always store a value first, and only - // after that do we touch the watchpoint set. There is a fence in the touch, that - // ensures that the store to the global object always happens before the touch on the - // set. - // - // Compilation thread: always first load the state of the watchpoint set, and then - // load the value. The WatchpointSet::state() method does fences for us to ensure - // that the load of the state happens before our load of the value. - // - // Finalizing compilation: this happens on the main thread and synchronously checks - // validity of all watchpoint sets. - // - // We will only perform optimizations if the load of the state yields IsWatched. That - // means that at least one store would have happened to initialize the original value - // of the variable (that is, the value we'd like to constant fold to). There may be - // other stores that happen after that, but those stores will invalidate the - // watchpoint set and also the compilation. - - // Note that we need to use the operand, which is a direct pointer at the global, - // rather than looking up the global by doing variableAt(offset). That's because the - // internal data structures of JSSegmentedVariableObject are not thread-safe even - // though accessing the global itself is. The segmentation involves a vector spine - // that resizes with malloc/free, so if new globals unrelated to the one we are - // reading are added, we might access freed memory if we do variableAt(). - WriteBarrier* pointer = std::bit_cast*>(operand); - JSValue value = pointer->get(); - if (value) { - m_graph.watchpoints().addLazily(*watchpoints); - set(bytecode.m_dst, weakJSConstant(value)); - break; - } - } - - SpeculatedType prediction = getPrediction(); - NodeType nodeType; - if (resolveType == GlobalVar || resolveType == GlobalVarWithVarInjectionChecks) - nodeType = GetGlobalVar; - else - nodeType = GetGlobalLexicalVariable; - Node* value = addToGraph(nodeType, OpInfo(operand), OpInfo(prediction)); - if (resolveType == GlobalLexicalVar || resolveType == GlobalLexicalVarWithVarInjectionChecks) - addToGraph(CheckNotEmpty, value); - set(bytecode.m_dst, value); - break; - } - case ResolvedClosureVar: - case ClosureVar: - case ClosureVarWithVarInjectionChecks: { - Node* scopeNode = get(bytecode.m_scope); - - // Ideally we wouldn't have to do this Phantom. But: - // - // For the constant case: we must do it because otherwise we would have no way of knowing - // that the scope is live at OSR here. - // - // For the non-constant case: GetClosureVar could be DCE'd, but baseline's implementation - // won't be able to handle an Undefined scope. - addToGraph(Phantom, scopeNode); - - // Constant folding in the bytecode parser is important for performance. This may not - // have executed yet. If it hasn't, then we won't have a prediction. Lacking a - // prediction, we'd otherwise think that it has to exit. Then when it did execute, we - // would recompile. But if we can fold it here, we avoid the exit. - if (JSValue value = m_graph.tryGetConstantClosureVar(scopeNode, ScopeOffset(operand))) { - set(bytecode.m_dst, weakJSConstant(value)); + case ModuleVar: + resolveInputs.lexicalEnvironment = metadata.m_lexicalEnvironment.get(); + break; + case ResolvedClosureVar: + case ClosureVar: + case ClosureVarWithVarInjectionChecks: + resolveInputs.symbolTable = metadata.m_symbolTable.get(); + break; + default: break; } - - SpeculatedType prediction = SpecNone; - if (bytecode.m_getPutInfo.resolveType() == ResolvedClosureVar) { - // ResolvedClosureVar is not used normally. It is very special internal ResolveType, mainly used for generators and private fields. - // In these variables, it can happen that we use JSEmpty as a result of op_get_from_scope (which becomes a TDZ error in normal ClosureVar). - // And this JSEmpty is still legit. The problem is that ValueProfile never tells about JSEmpty since it sees no value is stored when JSEmpty - // is stored. We workaround this very special internal use case by explicitly setting SpecEmpty when ValueProfile tells this is SpecNone. - prediction = getPredictionWithoutOSRExit(); - if (prediction == SpecNone) - prediction = SpecEmpty; - } else - prediction = getPrediction(); - set(bytecode.m_dst, addToGraph(GetClosureVar, OpInfo(operand), OpInfo(prediction), scopeNode)); - break; - } - case UnresolvedProperty: - case UnresolvedPropertyWithVarInjectionChecks: - case ModuleVar: - case Dynamic: - RELEASE_ASSERT_NOT_REACHED(); - break; - } - NEXT_OPCODE(op_get_from_scope); + getInputs.getPutInfo = metadata.m_getPutInfo; + ResolveType getType = getInputs.getPutInfo.resolveType(); + if (getType == GlobalVar || getType == GlobalVarWithVarInjectionChecks || getType == GlobalLexicalVar || getType == GlobalLexicalVarWithVarInjectionChecks) + getInputs.watchpoints = metadata.m_watchpointSet; + else if (getType == GlobalProperty || getType == GlobalPropertyWithVarInjectionChecks) + getInputs.structure = metadata.m_structureID.get(); + getInputs.operand = metadata.m_operand; + } + + Node* baseScope = get(bytecode.m_scope); + // Both halves live in one bytecode, so nothing that can exit may follow a node that clobbered the exit + // state. A dynamic resolve therefore becomes one GetDynamicVar that resolves first (no ResolveScope node), + // and after a ForceOSRExit the rest of the instruction is dead. + if (needsDynamicLookup(resolveInputs.resolveType, op_resolve_scope)) { + uint64_t opInfo1 = makeDynamicVarOpInfo(identifierNumber, getInputs.getPutInfo.operand() | GetPutInfo::resolvesScopeFirstBit); + set(bytecode.m_dst, addToGraph(GetDynamicVar, OpInfo(opInfo1), OpInfo(getPrediction()), baseScope)); + NEXT_OPCODE(op_resolve_and_get_from_scope); + } + ResolvedScope resolved = parseResolveScope(resolveInputs, baseScope); + if (resolved.kind == ResolveScopeKind::Exited) { + set(bytecode.m_dst, addToGraph(JSConstant, OpInfo(m_constantUndefined))); + NEXT_OPCODE(op_resolve_and_get_from_scope); + } + ASSERT(resolved.kind == ResolveScopeKind::Static); + set(bytecode.m_dst, parseGetFromScope(getInputs, resolved.node, baseScope, false)); + NEXT_OPCODE(op_resolve_and_get_from_scope); } case op_put_to_scope: { diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index 592e999cff13..6cce69333970 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -5969,6 +5969,10 @@ JSC_DEFINE_JIT_OPERATION(operationGetDynamicVar, EncodedJSValue, (JSGlobalObject auto scope = DECLARE_THROW_SCOPE(vm); Identifier ident = Identifier::fromUid(vm, impl); + if (getPutInfoBits & GetPutInfo::resolvesScopeFirstBit) { + jsScope = JSScope::resolve(globalObject, uncheckedDowncast(jsScope), ident); + OPERATION_RETURN_IF_EXCEPTION(scope, encodedJSValue()); + } OPERATION_RETURN(scope, JSValue::encode(jsScope->getPropertySlot(globalObject, ident, [&] (bool found, PropertySlot& slot) -> JSValue { if (!found) { GetPutInfo getPutInfo(getPutInfoBits); diff --git a/Source/JavaScriptCore/jit/BaselineJITRegisters.h b/Source/JavaScriptCore/jit/BaselineJITRegisters.h index daabf5302e69..770ccee2cc02 100644 --- a/Source/JavaScriptCore/jit/BaselineJITRegisters.h +++ b/Source/JavaScriptCore/jit/BaselineJITRegisters.h @@ -137,6 +137,10 @@ namespace PutToScope { static constexpr GPRReg bytecodeOffsetGPR { GPRInfo::argumentGPR2 }; } +namespace ResolveAndGetFromScope { + static constexpr GPRReg bytecodeOffsetGPR { GPRInfo::argumentGPR2 }; +} + namespace GetById { // Registers used on both Fast and Slow paths using SlowOperation = decltype(operationGetByIdOptimize); diff --git a/Source/JavaScriptCore/jit/JIT.cpp b/Source/JavaScriptCore/jit/JIT.cpp index 4ea7e1ff89a9..2666151db462 100644 --- a/Source/JavaScriptCore/jit/JIT.cpp +++ b/Source/JavaScriptCore/jit/JIT.cpp @@ -440,6 +440,7 @@ void JIT::privateCompileMainPass() DEFINE_OP(op_resolve_scope) DEFINE_OP(op_get_from_scope) + DEFINE_OP(op_resolve_and_get_from_scope) DEFINE_OP(op_put_to_scope) DEFINE_OP(op_get_from_arguments) DEFINE_OP(op_put_to_arguments) @@ -574,6 +575,7 @@ void JIT::privateCompileSlowCases() DEFINE_SLOWCASE_OP(op_sub) DEFINE_SLOWCASE_OP(op_resolve_scope) DEFINE_SLOWCASE_OP(op_get_from_scope) + DEFINE_SLOWCASE_OP(op_resolve_and_get_from_scope) DEFINE_SLOWCASE_OP(op_put_to_scope) DEFINE_SLOWCASE_OP(op_iterator_open) diff --git a/Source/JavaScriptCore/jit/JIT.h b/Source/JavaScriptCore/jit/JIT.h index ba17ffec0834..29a75156c5e2 100644 --- a/Source/JavaScriptCore/jit/JIT.h +++ b/Source/JavaScriptCore/jit/JIT.h @@ -604,6 +604,8 @@ namespace JSC { void emitSlow_op_resolve_scope(const JSInstruction*, Vector::iterator&); void emit_op_get_from_scope(const JSInstruction*); void emitSlow_op_get_from_scope(const JSInstruction*, Vector::iterator&); + void emit_op_resolve_and_get_from_scope(const JSInstruction*); + void emitSlow_op_resolve_and_get_from_scope(const JSInstruction*, Vector::iterator&); void emit_op_put_to_scope(const JSInstruction*); void emit_op_get_from_arguments(const JSInstruction*); void emit_op_put_to_arguments(const JSInstruction*); @@ -654,14 +656,27 @@ namespace JSC { private: static MacroAssemblerCodeRef slow_op_put_to_scopeGenerator(VM&); + static MacroAssemblerCodeRef slow_op_resolve_and_get_from_scopeGenerator(VM&); static MacroAssemblerCodeRef op_throw_handlerGenerator(VM&); static MacroAssemblerCodeRef op_check_traps_handlerGenerator(VM&); + // The scope-access thunks serve resolve_scope / get_from_scope and the two halves of resolve_and_get_from_scope + // (whose metadata lays the two ops' metadata back to back); the opcode picks the slow operation. + template static MacroAssemblerCodeRef slow_op_get_from_scopeGenerator(VM&); + template static MacroAssemblerCodeRef slow_op_resolve_scopeGenerator(VM&); - template + template static MacroAssemblerCodeRef generateOpGetFromScopeThunk(VM&); - template + template static MacroAssemblerCodeRef generateOpResolveScopeThunk(VM&); + template + void emitResolveScopeHalf(const Op&, VirtualRegister scope, ResolveType profiledResolveType); + template + void emitSlowResolveScopeHalf(const Op&, VirtualRegister scope, ResolveType profiledResolveType); + template + void emitGetFromScopeHalf(const Op&, std::optional scope, ResolveType profiledResolveType); + template + void emitSlowGetFromScopeHalf(const Op&, std::optional scope, ResolveType profiledResolveType); static MacroAssemblerCodeRef op_enter_handlerGenerator(VM&); static MacroAssemblerCodeRef valueIsTruthyGenerator(VM&); static MacroAssemblerCodeRef valueIsFalseyGenerator(VM&); diff --git a/Source/JavaScriptCore/jit/JITOperations.cpp b/Source/JavaScriptCore/jit/JITOperations.cpp index 2f2475fded76..6fc9d7fe9afa 100644 --- a/Source/JavaScriptCore/jit/JITOperations.cpp +++ b/Source/JavaScriptCore/jit/JITOperations.cpp @@ -4579,16 +4579,16 @@ JSC_DEFINE_JIT_OPERATION(operationSwitchStringWithUnknownKeyType, char*, (JSGlob OPERATION_RETURN(scope, reinterpret_cast(result)); } -JSC_DEFINE_JIT_OPERATION(operationResolveScopeForBaseline, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc)) +// The resolve_scope slow path, also the resolve half of resolve_and_get_from_scope (same metadata fields). The caller +// is the operation JIT code called into; it has already set up the frame tracer. +template +static OperationReturnType resolveScopeForBaseline(VM& vm, CallFrame* callFrame, JSGlobalObject* globalObject, const JSInstruction* pc) { - VM& vm = globalObject->vm(); - CallFrame* callFrame = DECLARE_CALL_FRAME(vm); - JITOperationPrologueCallFrameTracer tracer(vm, callFrame); auto scope = DECLARE_THROW_SCOPE(vm); CodeBlock* codeBlock = callFrame->codeBlock(); - auto bytecode = pc->as(); + auto bytecode = pc->as(); const Identifier& ident = codeBlock->identifier(bytecode.m_var); JSScope* environment = callFrame->uncheckedR(bytecode.m_scope).Register::scope(); JSObject* resolvedScope = JSScope::resolve(globalObject, environment, ident); @@ -4631,19 +4631,35 @@ JSC_DEFINE_JIT_OPERATION(operationResolveScopeForBaseline, EncodedJSValue, (JSGl OPERATION_RETURN(scope, JSValue::encode(resolvedScope)); } -JSC_DEFINE_JIT_OPERATION(operationGetFromScope, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc)) +JSC_DEFINE_JIT_OPERATION(operationResolveScopeForBaseline, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc)) +{ + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + return resolveScopeForBaseline(vm, callFrame, globalObject, pc); +} + +JSC_DEFINE_JIT_OPERATION(operationResolveScopeHalfForBaseline, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc)) { VM& vm = globalObject->vm(); CallFrame* callFrame = DECLARE_CALL_FRAME(vm); JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + return resolveScopeForBaseline(vm, callFrame, globalObject, pc); +} + +// The get_from_scope slow path, also the get half of resolve_and_get_from_scope with the resolved scope passed in. +template +static OperationReturnType getFromScopeForBaseline(VM& vm, CallFrame* callFrame, JSGlobalObject* globalObject, const JSInstruction* pc, JSObject* environment) +{ auto scope = DECLARE_THROW_SCOPE(vm); CodeBlock* codeBlock = callFrame->codeBlock(); - auto bytecode = pc->as(); + auto bytecode = pc->as(); const Identifier& ident = codeBlock->identifier(bytecode.m_var); - JSObject* environment = uncheckedDowncast(callFrame->uncheckedR(bytecode.m_scope).jsValue()); GetPutInfo& getPutInfo = bytecode.metadata(codeBlock).m_getPutInfo; + if constexpr (std::is_same_v) + environment = uncheckedDowncast(callFrame->uncheckedR(bytecode.m_scope).jsValue()); // ModuleVar is always converted to ClosureVar for get_from_scope. ASSERT(getPutInfo.resolveType() != ModuleVar); @@ -4673,6 +4689,37 @@ JSC_DEFINE_JIT_OPERATION(operationGetFromScope, EncodedJSValue, (JSGlobalObject* }))); } +JSC_DEFINE_JIT_OPERATION(operationGetFromScope, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc)) +{ + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + return getFromScopeForBaseline(vm, callFrame, globalObject, pc, nullptr); +} + +JSC_DEFINE_JIT_OPERATION(operationGetFromScopeHalf, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc, JSObject* environment)) +{ + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + return getFromScopeForBaseline(vm, callFrame, globalObject, pc, environment); +} + +JSC_DEFINE_JIT_OPERATION(operationResolveAndGetFromScope, EncodedJSValue, (JSGlobalObject* globalObject, const JSInstruction* pc)) +{ + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + auto scope = DECLARE_THROW_SCOPE(vm); + + CodeBlock* codeBlock = callFrame->codeBlock(); + auto bytecode = pc->as(); + JSScope* baseScope = callFrame->uncheckedR(bytecode.m_scope).Register::scope(); + JSValue result = CommonSlowPaths::resolveAndGetFromScopeSlow(globalObject, codeBlock, vm, bytecode, baseScope); + OPERATION_RETURN_IF_EXCEPTION(scope, encodedJSValue()); + OPERATION_RETURN(scope, JSValue::encode(result)); +} + JSC_DEFINE_JIT_OPERATION(operationPutToScope, void, (JSGlobalObject* globalObject, const JSInstruction* pc)) { VM& vm = globalObject->vm(); diff --git a/Source/JavaScriptCore/jit/JITOperations.h b/Source/JavaScriptCore/jit/JITOperations.h index 1dfb2154aacf..99382d1abd12 100644 --- a/Source/JavaScriptCore/jit/JITOperations.h +++ b/Source/JavaScriptCore/jit/JITOperations.h @@ -385,6 +385,9 @@ JSC_DECLARE_JIT_OPERATION(operationSetupVarargsFrame, CallFrame*, (JSGlobalObjec JSC_DECLARE_JIT_OPERATION(operationSwitchStringWithUnknownKeyType, char*, (JSGlobalObject*, EncodedJSValue key, size_t tableIndex)); JSC_DECLARE_JIT_OPERATION(operationResolveScopeForBaseline, EncodedJSValue, (JSGlobalObject*, const JSInstruction* bytecodePC)); JSC_DECLARE_JIT_OPERATION(operationGetFromScope, EncodedJSValue, (JSGlobalObject*, const JSInstruction* bytecodePC)); +JSC_DECLARE_JIT_OPERATION(operationResolveScopeHalfForBaseline, EncodedJSValue, (JSGlobalObject*, const JSInstruction* bytecodePC)); +JSC_DECLARE_JIT_OPERATION(operationGetFromScopeHalf, EncodedJSValue, (JSGlobalObject*, const JSInstruction* bytecodePC, JSObject* scope)); +JSC_DECLARE_JIT_OPERATION(operationResolveAndGetFromScope, EncodedJSValue, (JSGlobalObject*, const JSInstruction* bytecodePC)); JSC_DECLARE_JIT_OPERATION(operationPutToScope, void, (JSGlobalObject*, const JSInstruction* bytecodePC)); JSC_DECLARE_JIT_OPERATION(operationResolveRope, StringImpl*, (JSGlobalObject*, JSString*)); JSC_DECLARE_JIT_OPERATION(operationResolveRopeString, JSString*, (JSGlobalObject*, JSRopeString*)); diff --git a/Source/JavaScriptCore/jit/JITPropertyAccess.cpp b/Source/JavaScriptCore/jit/JITPropertyAccess.cpp index e8e5e0835fb6..602f70dbc10e 100644 --- a/Source/JavaScriptCore/jit/JITPropertyAccess.cpp +++ b/Source/JavaScriptCore/jit/JITPropertyAccess.cpp @@ -876,22 +876,18 @@ void JIT::emitSlow_op_has_private_brand(const JSInstruction*, Vector +void JIT::emitResolveScopeHalf(const Op& bytecode, VirtualRegister scope, ResolveType profiledResolveType) { - auto bytecode = currentInstruction->as(); - ResolveType profiledResolveType = bytecode.metadata(m_profiledCodeBlock).m_resolveType; - VirtualRegister dst = bytecode.m_dst; - VirtualRegister scope = bytecode.m_scope; - + // Leaves the resolved scope (an unboxed cell) in returnValueGPR. uint32_t bytecodeOffset = m_bytecodeIndex.offset(); ASSERT(BytecodeIndex(m_bytecodeIndex.offset()) == m_bytecodeIndex); - ASSERT(m_unlinkedCodeBlock->instructionAt(m_bytecodeIndex) == currentInstruction); using BaselineJITRegisters::ResolveScope::scopeGPR; using BaselineJITRegisters::ResolveScope::bytecodeOffsetGPR; using BaselineJITRegisters::ResolveScope::scratch1GPR; using BaselineJITRegisters::ResolveScope::metadataGPR; - using Metadata = OpResolveScope::Metadata; + using Metadata = typename Op::Metadata; // If we profile certain resolve types, we're guaranteed all linked code will have the same // resolve type. @@ -952,15 +948,15 @@ void JIT::emit_op_resolve_scope(const JSInstruction* currentInstruction) MacroAssemblerCodeRef code; if (profiledResolveType == ClosureVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalPropertyWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalLexicalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); emitGetVirtualRegister(scope, scopeGPR); move(TrustedImm32(bytecodeOffset), bytecodeOffsetGPR); @@ -969,19 +965,22 @@ void JIT::emit_op_resolve_scope(const JSInstruction* currentInstruction) } } } +} + +void JIT::emit_op_resolve_scope(const JSInstruction* currentInstruction) +{ + auto bytecode = currentInstruction->as(); + ASSERT(m_unlinkedCodeBlock->instructionAt(m_bytecodeIndex) == currentInstruction); + emitResolveScopeHalf(bytecode, bytecode.m_scope, bytecode.metadata(m_profiledCodeBlock).m_resolveType); setFastPathResumePoint(); boxCell(returnValueGPR, returnValueJSR); - emitPutVirtualRegister(dst, returnValueJSR); + emitPutVirtualRegister(bytecode.m_dst, returnValueJSR); } -void JIT::emitSlow_op_resolve_scope(const JSInstruction* currentInstruction, Vector::iterator& iter) +template +void JIT::emitSlowResolveScopeHalf(const Op& bytecode, VirtualRegister scope, ResolveType profiledResolveType) { - linkAllSlowCases(iter); - - auto bytecode = currentInstruction->as(); - VirtualRegister scope = bytecode.m_scope; - ResolveType profiledResolveType = bytecode.metadata(m_profiledCodeBlock).m_resolveType; uint32_t bytecodeOffset = m_bytecodeIndex.offset(); using BaselineJITRegisters::ResolveScope::metadataGPR; @@ -997,28 +996,36 @@ void JIT::emitSlow_op_resolve_scope(const JSInstruction* currentInstruction, Vec MacroAssemblerCodeRef code; // FIXME: Why do we generate the cases for the thunks we already emitted in the fast path. It seems like those should just go straight to the generic slow path thunk. if (profiledResolveType == ClosureVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalVar) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalProperty) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalLexicalVar) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalPropertyWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else if (profiledResolveType == GlobalLexicalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); else - code = vm().getCTIStub(generateOpResolveScopeThunk); + code = vm().getCTIStub(generateOpResolveScopeThunk); emitGetVirtualRegister(scope, scopeGPR); move(TrustedImm32(bytecodeOffset), bytecodeOffsetGPR); nearCallThunk(CodeLocationLabel { code.retaggedCode() }); } -template +void JIT::emitSlow_op_resolve_scope(const JSInstruction* currentInstruction, Vector::iterator& iter) +{ + linkAllSlowCases(iter); + + auto bytecode = currentInstruction->as(); + emitSlowResolveScopeHalf(bytecode, bytecode.m_scope, bytecode.metadata(m_profiledCodeBlock).m_resolveType); +} + +template MacroAssemblerCodeRef JIT::generateOpResolveScopeThunk(VM& vm) { // The thunk generated by this function can only work with the LLInt / Baseline JIT because @@ -1028,7 +1035,7 @@ MacroAssemblerCodeRef JIT::generateOpResolveScopeThunk(VM& vm) CCallHelpers jit; - using Metadata = OpResolveScope::Metadata; + using Metadata = typename Op::Metadata; using BaselineJITRegisters::ResolveScope::metadataGPR; // Incoming using BaselineJITRegisters::ResolveScope::scopeGPR; // Incoming using BaselineJITRegisters::ResolveScope::bytecodeOffsetGPR; // Incoming - pass through to slow path. @@ -1148,12 +1155,13 @@ MacroAssemblerCodeRef JIT::generateOpResolveScopeThunk(VM& vm) jit.ret(); - slowCase.linkThunk(CodeLocationLabel { vm.getCTIStub(slow_op_resolve_scopeGenerator).retaggedCode() }, &jit); + slowCase.linkThunk(CodeLocationLabel { vm.getCTIStub(slow_op_resolve_scopeGenerator).template retaggedCode() }, &jit); LinkBuffer patchBuffer(jit, GLOBAL_THUNK_ID, LinkBuffer::Profile::ExtraCTIThunk); return FINALIZE_THUNK(patchBuffer, JITThunkPtrTag, "resolve_scope"_s, "Baseline: resolve_scope"); } +template MacroAssemblerCodeRef JIT::slow_op_resolve_scopeGenerator(VM& vm) { // The thunk generated by this function can only work with the LLInt / Baseline JIT because @@ -1178,8 +1186,13 @@ MacroAssemblerCodeRef JIT::slow_op_resolve_scopeGenerator(VM& vm jit.loadPtr(Address(scratch1GPR, CodeBlock::offsetOfGlobalObject()), globalObjectGPR); jit.loadPtr(Address(scratch1GPR, CodeBlock::offsetOfInstructionsRawPointer()), instructionGPR); jit.addPtr(bytecodeOffsetGPR, instructionGPR); - jit.setupArguments(globalObjectGPR, instructionGPR); - jit.callOperation(operationResolveScopeForBaseline); + if constexpr (std::is_same_v) { + jit.setupArguments(globalObjectGPR, instructionGPR); + jit.callOperation(operationResolveScopeForBaseline); + } else { + jit.setupArguments(globalObjectGPR, instructionGPR); + jit.callOperation(operationResolveScopeHalfForBaseline); + } jit.emitCTIThunkEpilogue(); @@ -1190,26 +1203,27 @@ MacroAssemblerCodeRef JIT::slow_op_resolve_scopeGenerator(VM& vm return FINALIZE_THUNK(patchBuffer, JITThunkPtrTag, "slow_op_resolve_scope"_s, "Baseline: slow_op_resolve_scope"); } -void JIT::emit_op_get_from_scope(const JSInstruction* currentInstruction) +// `scope` is the bytecode's scope register, or nothing when the scope is already in GetFromScope::scopeGPR. +template +void JIT::emitGetFromScopeHalf(const Op& bytecode, std::optional scope, ResolveType profiledResolveType) { - auto bytecode = currentInstruction->as(); - VirtualRegister dst = bytecode.m_dst; - VirtualRegister scope = bytecode.m_scope; - ResolveType profiledResolveType = bytecode.metadata(m_profiledCodeBlock).m_getPutInfo.resolveType(); - uint32_t bytecodeOffset = m_bytecodeIndex.offset(); ASSERT(BytecodeIndex(m_bytecodeIndex.offset()) == m_bytecodeIndex); - ASSERT(m_unlinkedCodeBlock->instructionAt(m_bytecodeIndex) == currentInstruction); - using Metadata = OpGetFromScope::Metadata; + using Metadata = typename Op::Metadata; using BaselineJITRegisters::GetFromScope::metadataGPR; using BaselineJITRegisters::GetFromScope::scopeGPR; using BaselineJITRegisters::GetFromScope::bytecodeOffsetGPR; using BaselineJITRegisters::GetFromScope::scratch1GPR; + auto loadScope = [&] { + if (scope) + emitGetVirtualRegister(*scope, scopeGPR); + }; + if (profiledResolveType == ClosureVar) { - emitGetVirtualRegister(scope, scopeGPR); + loadScope(); loadPtrFromMetadata(bytecode, Metadata::offsetOfOperand(), scratch1GPR); loadValue(BaseIndex(scopeGPR, scratch1GPR, TimesEight, JSLexicalEnvironment::offsetOfVariables()), returnValueJSR); } else { @@ -1234,7 +1248,7 @@ void JIT::emit_op_get_from_scope(const JSInstruction* currentInstruction) case GlobalProperty: { addSlowCase(branch32(NotEqual, scratch1GPR, TrustedImm32(profiledResolveType))); load32(structureIDAddress, scratch1GPR); - emitGetVirtualRegister(scope, scopeGPR); + loadScope(); addSlowCase(branch32(NotEqual, Address(scopeGPR, JSCell::structureIDOffset()), scratch1GPR)); loadPtr(operandAddress, scratch1GPR); loadPtr(Address(scopeGPR, JSObject::butterflyOffset()), scopeGPR); @@ -1264,46 +1278,48 @@ void JIT::emit_op_get_from_scope(const JSInstruction* currentInstruction) MacroAssemblerCodeRef code; if (profiledResolveType == ClosureVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpGetFromScopeThunk); - if (profiledResolveType == GlobalProperty) - code = vm().getCTIStub(generateOpGetFromScopeThunk); - if (profiledResolveType == GlobalVar) - code = vm().getCTIStub(generateOpGetFromScopeThunk); - if (profiledResolveType == GlobalLexicalVar) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); + else if (profiledResolveType == GlobalProperty) + code = vm().getCTIStub(generateOpGetFromScopeThunk); + else if (profiledResolveType == GlobalVar) + code = vm().getCTIStub(generateOpGetFromScopeThunk); + else if (profiledResolveType == GlobalLexicalVar) + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalLexicalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); - emitGetVirtualRegister(scope, scopeGPR); + loadScope(); move(TrustedImm32(bytecodeOffset), bytecodeOffsetGPR); nearCallThunk(CodeLocationLabel { code.retaggedCode() }); break; } } } +} + +void JIT::emit_op_get_from_scope(const JSInstruction* currentInstruction) +{ + auto bytecode = currentInstruction->as(); + ASSERT(m_unlinkedCodeBlock->instructionAt(m_bytecodeIndex) == currentInstruction); + emitGetFromScopeHalf(bytecode, bytecode.m_scope, bytecode.metadata(m_profiledCodeBlock).m_getPutInfo.resolveType()); setFastPathResumePoint(); emitValueProfilingSite(bytecode, returnValueJSR); - emitPutVirtualRegister(dst, returnValueJSR); + emitPutVirtualRegister(bytecode.m_dst, returnValueJSR); } -void JIT::emitSlow_op_get_from_scope(const JSInstruction* currentInstruction, Vector::iterator& iter) +template +void JIT::emitSlowGetFromScopeHalf(const Op& bytecode, std::optional scope, ResolveType profiledResolveType) { - linkAllSlowCases(iter); - - auto bytecode = currentInstruction->as(); - VirtualRegister scope = bytecode.m_scope; - ResolveType profiledResolveType = bytecode.metadata(m_profiledCodeBlock).m_getPutInfo.resolveType(); uint32_t bytecodeOffset = m_bytecodeIndex.offset(); using BaselineJITRegisters::GetFromScope::metadataGPR; using BaselineJITRegisters::GetFromScope::scopeGPR; using BaselineJITRegisters::GetFromScope::bytecodeOffsetGPR; - using BaselineJITRegisters::GetFromScope::scratch1GPR; // Materialize metadataGPR if we didn't already. constexpr size_t metadataMinAlignment = 4; @@ -1314,34 +1330,43 @@ void JIT::emitSlow_op_get_from_scope(const JSInstruction* currentInstruction, Ve MacroAssemblerCodeRef code; if (profiledResolveType == ClosureVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalVar) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalProperty) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalLexicalVar) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else if (profiledResolveType == GlobalLexicalVarWithVarInjectionChecks) - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); else - code = vm().getCTIStub(generateOpGetFromScopeThunk); + code = vm().getCTIStub(generateOpGetFromScopeThunk); - emitGetVirtualRegister(scope, scopeGPR); + if (scope) + emitGetVirtualRegister(*scope, scopeGPR); addPtr(TrustedImm32(metadataOffset), GPRInfo::metadataTableRegister, metadataGPR); move(TrustedImm32(bytecodeOffset), bytecodeOffsetGPR); nearCallThunk(CodeLocationLabel { code.retaggedCode() }); } -template +void JIT::emitSlow_op_get_from_scope(const JSInstruction* currentInstruction, Vector::iterator& iter) +{ + linkAllSlowCases(iter); + + auto bytecode = currentInstruction->as(); + emitSlowGetFromScopeHalf(bytecode, bytecode.m_scope, bytecode.metadata(m_profiledCodeBlock).m_getPutInfo.resolveType()); +} + +template MacroAssemblerCodeRef JIT::generateOpGetFromScopeThunk(VM& vm) { // The thunk generated by this function can only work with the LLInt / Baseline JIT because // it makes assumptions about the right globalObject being available from CallFrame::codeBlock(). // DFG/FTL may inline functions belonging to other globalObjects, which may not match // CallFrame::codeBlock(). - using Metadata = OpGetFromScope::Metadata; + using Metadata = typename Op::Metadata; using BaselineJITRegisters::GetFromScope::metadataGPR; // Incoming using BaselineJITRegisters::GetFromScope::scopeGPR; // Incoming @@ -1368,7 +1393,7 @@ MacroAssemblerCodeRef JIT::generateOpGetFromScopeThunk(VM& vm) case GlobalProperty: case GlobalPropertyWithVarInjectionChecks: { // Structure check covers var injection since we don't cache structures for anything but the GlobalObject. Additionally, resolve_scope handles checking for the var injection. - jit.load32(Address(metadataGPR, OpGetFromScope::Metadata::offsetOfStructureID()), scratch1GPR); + jit.load32(Address(metadataGPR, Metadata::offsetOfStructureID()), scratch1GPR); slowCase.append(jit.branch32(NotEqual, Address(scopeGPR, JSCell::structureIDOffset()), scratch1GPR)); jit.jitAssert(scopedLambda([&] () -> Jump { @@ -1459,12 +1484,13 @@ MacroAssemblerCodeRef JIT::generateOpGetFromScopeThunk(VM& vm) jit.ret(); - slowCase.linkThunk(CodeLocationLabel { vm.getCTIStub(slow_op_get_from_scopeGenerator).retaggedCode() }, &jit); + slowCase.linkThunk(CodeLocationLabel { vm.getCTIStub(slow_op_get_from_scopeGenerator).template retaggedCode() }, &jit); LinkBuffer patchBuffer(jit, GLOBAL_THUNK_ID, LinkBuffer::Profile::ExtraCTIThunk); return FINALIZE_THUNK(patchBuffer, JITThunkPtrTag, "get_from_scope"_s, "Baseline: get_from_scope"); } +template MacroAssemblerCodeRef JIT::slow_op_get_from_scopeGenerator(VM& vm) { // The thunk generated by this function can only work with the LLInt / Baseline JIT because @@ -1474,11 +1500,14 @@ MacroAssemblerCodeRef JIT::slow_op_get_from_scopeGenerator(VM& v CCallHelpers jit; using BaselineJITRegisters::GetFromScope::metadataGPR; // Incoming + using BaselineJITRegisters::GetFromScope::scopeGPR; // Incoming using BaselineJITRegisters::GetFromScope::bytecodeOffsetGPR; // Incoming constexpr GPRReg globalObjectGPR = argumentGPR0; constexpr GPRReg instructionGPR = argumentGPR1; + constexpr GPRReg scopeArgumentGPR = argumentGPR2; static_assert(noOverlap(metadataGPR, bytecodeOffsetGPR, globalObjectGPR, instructionGPR)); static_assert(noOverlap(metadataGPR, returnValueGPR)); + static_assert(noOverlap(scopeGPR, globalObjectGPR, instructionGPR)); jit.emitCTIThunkPrologue(/* returnAddressAlreadyTagged: */ true); // Return address tagged in 'generateOpGetFromScopeThunk' @@ -1494,7 +1523,13 @@ MacroAssemblerCodeRef JIT::slow_op_get_from_scopeGenerator(VM& v jit.subPtr(TrustedImmPtr(16), stackPointerRegister); jit.storePtr(metadataGPR, Address(stackPointerRegister)); - jit.callOperation(operationGetFromScope); + if constexpr (std::is_same_v) + jit.callOperation(operationGetFromScope); + else { + // The resolved scope lives only in scopeGPR. + jit.move(scopeGPR, scopeArgumentGPR); + jit.callOperation(operationGetFromScopeHalf); + } Jump exceptionCheck = jit.emitNonPatchableExceptionCheck(vm); jit.loadPtr(Address(stackPointerRegister), metadataGPR); // Restore metadataGPR @@ -1512,6 +1547,67 @@ MacroAssemblerCodeRef JIT::slow_op_get_from_scopeGenerator(VM& v return FINALIZE_THUNK(patchBuffer, JITThunkPtrTag, "slow_op_get_from_scope"_s, "Baseline: slow_op_get_from_scope"); } +void JIT::emit_op_resolve_and_get_from_scope(const JSInstruction* currentInstruction) +{ + auto bytecode = currentInstruction->as(); + ASSERT(m_unlinkedCodeBlock->instructionAt(m_bytecodeIndex) == currentInstruction); + auto& metadata = bytecode.metadata(m_profiledCodeBlock); + + emitResolveScopeHalf(bytecode, bytecode.m_scope, metadata.m_resolveType); + move(returnValueGPR, BaselineJITRegisters::GetFromScope::scopeGPR); + emitGetFromScopeHalf(bytecode, std::nullopt, metadata.m_getPutInfo.resolveType()); + + setFastPathResumePoint(); + emitValueProfilingSite(bytecode, returnValueJSR); + emitPutVirtualRegister(bytecode.m_dst, returnValueJSR); +} + +void JIT::emitSlow_op_resolve_and_get_from_scope(const JSInstruction* currentInstruction, Vector::iterator& iter) +{ + linkAllSlowCases(iter); + + // Either half failed its inline checks: redo the whole instruction in C++. + uint32_t bytecodeOffset = m_bytecodeIndex.offset(); + ASSERT(BytecodeIndex(m_bytecodeIndex.offset()) == m_bytecodeIndex); + ASSERT(m_unlinkedCodeBlock->instructionAt(m_bytecodeIndex) == currentInstruction); + + using BaselineJITRegisters::ResolveAndGetFromScope::bytecodeOffsetGPR; + + move(TrustedImm32(bytecodeOffset), bytecodeOffsetGPR); + nearCallThunk(CodeLocationLabel { vm().getCTIStub(slow_op_resolve_and_get_from_scopeGenerator).retaggedCode() }); +} + +MacroAssemblerCodeRef JIT::slow_op_resolve_and_get_from_scopeGenerator(VM& vm) +{ + // Same shape as slow_op_put_to_scopeGenerator: the operation reads everything from the frame and the instruction. + CCallHelpers jit; + + constexpr GPRReg globalObjectGPR = argumentGPR0; + constexpr GPRReg instructionGPR = argumentGPR1; + using BaselineJITRegisters::ResolveAndGetFromScope::bytecodeOffsetGPR; // Incoming + constexpr GPRReg codeBlockGPR = argumentGPR3; // Only used as scratch register + static_assert(noOverlap(globalObjectGPR, instructionGPR, bytecodeOffsetGPR, codeBlockGPR)); + + jit.emitCTIThunkPrologue(); + + jit.store32(bytecodeOffsetGPR, highWordFor(CallFrameSlot::argumentCountIncludingThis)); + jit.prepareCallOperation(vm); + jit.loadPtr(addressFor(CallFrameSlot::codeBlock), codeBlockGPR); + jit.loadPtr(Address(codeBlockGPR, CodeBlock::offsetOfGlobalObject()), globalObjectGPR); + jit.loadPtr(Address(codeBlockGPR, CodeBlock::offsetOfInstructionsRawPointer()), instructionGPR); + jit.addPtr(bytecodeOffsetGPR, instructionGPR); + jit.setupArguments(globalObjectGPR, instructionGPR); + jit.callOperation(operationResolveAndGetFromScope); + + jit.emitCTIThunkEpilogue(); + + // Tail call to exception check thunk + jit.jumpThunk(CodeLocationLabel(vm.getCTIStub(CommonJITThunkID::CheckException).retaggedCode())); + + LinkBuffer patchBuffer(jit, GLOBAL_THUNK_ID, LinkBuffer::Profile::ExtraCTIThunk); + return FINALIZE_THUNK(patchBuffer, JITThunkPtrTag, "slow_op_resolve_and_get_from_scope"_s, "Baseline: slow_op_resolve_and_get_from_scope"); +} + void JIT::emit_op_put_to_scope(const JSInstruction* currentInstruction) { auto bytecode = currentInstruction->as(); diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm index 57d8b0403d8e..233573805e04 100644 --- a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm +++ b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm @@ -2965,6 +2965,162 @@ llintOpWithMetadata(op_get_from_scope, OpGetFromScope, macro (size, get, dispatc end) +# resolve_scope then get_from_scope, with the resolved scope in t3 instead of a register. Either half that cannot be +# served inline goes to the one slow path, which does both from the start. +llintOpWithMetadata(op_resolve_and_get_from_scope, OpResolveAndGetFromScope, macro (size, get, dispatch, metadata, return) + metadata(t5, t0) + + macro loadConstantScope() + loadp OpResolveAndGetFromScope::Metadata::m_constantScope[t5], t3 + end + + macro epochCheck(slowPath, globalObject, scratch) + loadi OpResolveAndGetFromScope::Metadata::m_globalLexicalBindingEpoch[t5], scratch + bineq JSGlobalObject::m_globalLexicalBindingEpoch[globalObject], scratch, slowPath + end + + macro walkToScope() + loadi OpResolveAndGetFromScope::Metadata::m_localScopeDepth[t5], t2 + get(m_scope, t3) + loadq [cfr, t3, 8], t3 + btiz t2, .walkEnd + .walkLoop: + loadp JSScope::m_next[t3], t3 + subi 1, t2 + btinz t2, .walkLoop + .walkEnd: + end + + loadi OpResolveAndGetFromScope::Metadata::m_resolveType[t5], t0 + +#rgrGlobalProperty: + bineq t0, GlobalProperty, .rgrGlobalVar + loadConstantScope() + epochCheck(.rgDynamic, t3, t2) + jmp .rgGet + +.rgrGlobalVar: + bineq t0, GlobalVar, .rgrGlobalLexicalVar + loadConstantScope() + jmp .rgGet + +.rgrGlobalLexicalVar: + bineq t0, GlobalLexicalVar, .rgrClosureVar + loadConstantScope() + jmp .rgGet + +.rgrClosureVar: + bineq t0, ClosureVar, .rgrModuleVar + walkToScope() + jmp .rgGet + +.rgrModuleVar: + bineq t0, ModuleVar, .rgrGlobalPropertyWithVarInjectionChecks + loadp OpResolveAndGetFromScope::Metadata::m_lexicalEnvironment[t5], t3 + jmp .rgGet + +.rgrGlobalPropertyWithVarInjectionChecks: + bineq t0, GlobalPropertyWithVarInjectionChecks, .rgrGlobalVarWithVarInjectionChecks + varInjectionCheck(.rgDynamic, t2) + loadConstantScope() + epochCheck(.rgDynamic, t3, t2) + jmp .rgGet + +.rgrGlobalVarWithVarInjectionChecks: + bineq t0, GlobalVarWithVarInjectionChecks, .rgrGlobalLexicalVarWithVarInjectionChecks + varInjectionCheck(.rgDynamic, t2) + loadConstantScope() + jmp .rgGet + +.rgrGlobalLexicalVarWithVarInjectionChecks: + bineq t0, GlobalLexicalVarWithVarInjectionChecks, .rgrClosureVarWithVarInjectionChecks + varInjectionCheck(.rgDynamic, t2) + loadConstantScope() + jmp .rgGet + +.rgrClosureVarWithVarInjectionChecks: + bineq t0, ClosureVarWithVarInjectionChecks, .rgDynamic + varInjectionCheck(.rgDynamic, t2) + walkToScope() + +.rgGet: + # t3 holds the resolved scope. Var injection was checked above, so the get half does not repeat it. + macro getProperty() + loadp OpResolveAndGetFromScope::Metadata::m_operand[t5], t1 + move t3, t0 + loadPropertyAtVariableOffset(t1, t0, t2) + valueProfile(size, OpResolveAndGetFromScope, m_valueProfile, t2, t5) + return(t2) + end + + macro getGlobalVar(tdzCheckIfNecessary) + loadp OpResolveAndGetFromScope::Metadata::m_operand[t5], t0 + loadq [t0], t0 + tdzCheckIfNecessary(t0) + valueProfile(size, OpResolveAndGetFromScope, m_valueProfile, t0, t5) + return(t0) + end + + macro getClosureVar() + loadp OpResolveAndGetFromScope::Metadata::m_operand[t5], t1 + loadq JSLexicalEnvironment_variables[t3, t1, 8], t0 + valueProfile(size, OpResolveAndGetFromScope, m_valueProfile, t0, t5) + return(t0) + end + + macro checkedGlobalProperty() + loadi JSCell::m_structureID[t3], t1 + bineq t1, OpResolveAndGetFromScope::Metadata::m_structureID[t5], .rgDynamic + getProperty() + end + + loadi OpResolveAndGetFromScope::Metadata::m_getPutInfo + GetPutInfo::m_operand[t5], t0 + andi ResolveTypeMask, t0 + +#rggGlobalProperty: + bineq t0, GlobalProperty, .rggGlobalVar + checkedGlobalProperty() + +.rggGlobalVar: + bineq t0, GlobalVar, .rggGlobalLexicalVar + getGlobalVar(macro(v) end) + +.rggGlobalLexicalVar: + bineq t0, GlobalLexicalVar, .rggClosureVar + getGlobalVar( + macro (value) + bqeq value, ValueEmpty, .rgDynamic + end) + +.rggClosureVar: + bineq t0, ClosureVar, .rggGlobalPropertyWithVarInjectionChecks + getClosureVar() + +.rggGlobalPropertyWithVarInjectionChecks: + bineq t0, GlobalPropertyWithVarInjectionChecks, .rggGlobalVarWithVarInjectionChecks + checkedGlobalProperty() + +.rggGlobalVarWithVarInjectionChecks: + bineq t0, GlobalVarWithVarInjectionChecks, .rggGlobalLexicalVarWithVarInjectionChecks + getGlobalVar(macro(v) end) + +.rggGlobalLexicalVarWithVarInjectionChecks: + bineq t0, GlobalLexicalVarWithVarInjectionChecks, .rggClosureVarWithVarInjectionChecks + getGlobalVar( + macro (value) + bqeq value, ValueEmpty, .rgDynamic + end) + +.rggClosureVarWithVarInjectionChecks: + bineq t0, ClosureVarWithVarInjectionChecks, .rgDynamic + getClosureVar() + +.rgDynamic: + callSlowPath(_slow_path_resolve_and_get_from_scope) + dispatch() +end) + + llintOpWithMetadata(op_put_to_scope, OpPutToScope, macro (size, get, dispatch, metadata, return) macro putProperty() get(m_value, t1) diff --git a/Source/JavaScriptCore/lol/LOLJIT.cpp b/Source/JavaScriptCore/lol/LOLJIT.cpp index 19efeace325b..9da41110d809 100644 --- a/Source/JavaScriptCore/lol/LOLJIT.cpp +++ b/Source/JavaScriptCore/lol/LOLJIT.cpp @@ -332,6 +332,7 @@ void LOLJIT::privateCompileMainPass() DEFINE_SLOW_OP(put_by_id_with_this) DEFINE_SLOW_OP(put_by_val_with_this) DEFINE_SLOW_OP(resolve_scope_for_hoisting_func_decl_in_eval) + DEFINE_SLOW_OP(resolve_and_get_from_scope) DEFINE_SLOW_OP(define_data_property) DEFINE_SLOW_OP(define_accessor_property) DEFINE_SLOW_OP(unreachable) diff --git a/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp b/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp index 3557e2387c8f..c66e10c560a5 100644 --- a/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp +++ b/Source/JavaScriptCore/runtime/CommonSlowPaths.cpp @@ -1383,6 +1383,16 @@ JSC_DEFINE_COMMON_SLOW_PATH(slow_path_resolve_scope_for_hoisting_func_decl_in_ev RETURN(resolvedScope); } +JSC_DEFINE_COMMON_SLOW_PATH(slow_path_resolve_and_get_from_scope) +{ + BEGIN(); + auto bytecode = pc->as(); + JSScope* baseScope = callFrame->uncheckedR(bytecode.m_scope).Register::scope(); + JSValue result = CommonSlowPaths::resolveAndGetFromScopeSlow(globalObject, codeBlock, vm, bytecode, baseScope); + CHECK_EXCEPTION(); + RETURN_PROFILED(result); +} + JSC_DEFINE_COMMON_SLOW_PATH(slow_path_resolve_scope) { BEGIN(); diff --git a/Source/JavaScriptCore/runtime/CommonSlowPaths.h b/Source/JavaScriptCore/runtime/CommonSlowPaths.h index 3d70e094a4d6..0f1d8a2e2d7f 100644 --- a/Source/JavaScriptCore/runtime/CommonSlowPaths.h +++ b/Source/JavaScriptCore/runtime/CommonSlowPaths.h @@ -310,6 +310,7 @@ JSC_DECLARE_COMMON_SLOW_PATH(slow_path_profile_type_clear_log); JSC_DECLARE_COMMON_SLOW_PATH(slow_path_unreachable); JSC_DECLARE_COMMON_SLOW_PATH(slow_path_push_with_scope); JSC_DECLARE_COMMON_SLOW_PATH(slow_path_resolve_scope); +JSC_DECLARE_COMMON_SLOW_PATH(slow_path_resolve_and_get_from_scope); JSC_DECLARE_COMMON_SLOW_PATH(slow_path_resolve_scope_for_hoisting_func_decl_in_eval); JSC_DECLARE_COMMON_SLOW_PATH(slow_path_create_promise); JSC_DECLARE_COMMON_SLOW_PATH(slow_path_create_generator); diff --git a/Source/JavaScriptCore/runtime/CommonSlowPathsInlines.h b/Source/JavaScriptCore/runtime/CommonSlowPathsInlines.h index 2119f944292c..f4aee3251347 100644 --- a/Source/JavaScriptCore/runtime/CommonSlowPathsInlines.h +++ b/Source/JavaScriptCore/runtime/CommonSlowPathsInlines.h @@ -119,8 +119,9 @@ inline void tryCachePutToScopeGlobal( } } +template inline void tryCacheGetFromScopeGlobal( - JSGlobalObject* globalObject, CodeBlock* codeBlock, VM& vm, OpGetFromScope& bytecode, JSObject* scope, PropertySlot& slot, const Identifier& ident) + JSGlobalObject* globalObject, CodeBlock* codeBlock, VM& vm, Bytecode& bytecode, JSObject* scope, PropertySlot& slot, const Identifier& ident) { auto& metadata = bytecode.metadata(codeBlock); ResolveType resolveType = metadata.m_getPutInfo.resolveType(); @@ -166,6 +167,87 @@ inline void tryCacheGetFromScopeGlobal( } } +// The resolve half of the fused op's slow path: what slow_path_resolve_scope does to its metadata once the scope is known. +inline void noteResolvedScope(CodeBlock* codeBlock, VM& vm, OpResolveAndGetFromScope::Metadata& metadata, JSObject* resolvedScope, const Identifier& ident, bool& hasPropertyCheckThrew) +{ + hasPropertyCheckThrew = false; + ResolveType resolveType = metadata.m_resolveType; + ASSERT(resolveType != ModuleVar); + switch (resolveType) { + case GlobalProperty: + case GlobalPropertyWithVarInjectionChecks: + case UnresolvedProperty: + case UnresolvedPropertyWithVarInjectionChecks: { + if (resolvedScope->isGlobalObject()) { + JSGlobalObject* globalObject = uncheckedDowncast(resolvedScope); + auto scope = DECLARE_THROW_SCOPE(vm); + bool hasProperty = globalObject->hasProperty(globalObject, ident); + if (scope.exception()) [[unlikely]] { + hasPropertyCheckThrew = true; + return; + } + if (hasProperty) { + ConcurrentJSLocker locker(codeBlock->m_lock); + metadata.m_resolveType = needsVarInjectionChecks(resolveType) ? GlobalPropertyWithVarInjectionChecks : GlobalProperty; + metadata.m_globalObject.set(vm, codeBlock, globalObject); + metadata.m_globalLexicalBindingEpoch = globalObject->globalLexicalBindingEpoch(); + } + } else if (resolvedScope->isGlobalLexicalEnvironment()) { + JSGlobalLexicalEnvironment* globalLexicalEnvironment = uncheckedDowncast(resolvedScope); + ConcurrentJSLocker locker(codeBlock->m_lock); + metadata.m_resolveType = needsVarInjectionChecks(resolveType) ? GlobalLexicalVarWithVarInjectionChecks : GlobalLexicalVar; + metadata.m_globalLexicalEnvironment.set(vm, codeBlock, globalLexicalEnvironment); + } + break; + } + default: + break; + } +} + +// The fused op's slow path: resolve from the base scope, then read the variable the way slow_path_get_from_scope does. +// Returns an empty value with an exception pending on failure. +inline JSValue resolveAndGetFromScopeSlow(JSGlobalObject* globalObject, CodeBlock* codeBlock, VM& vm, OpResolveAndGetFromScope& bytecode, JSScope* baseScope) +{ + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& metadata = bytecode.metadata(codeBlock); + const Identifier& ident = codeBlock->identifier(bytecode.m_var); + + JSObject* scope = JSScope::resolve(globalObject, baseScope, ident); + RETURN_IF_EXCEPTION(throwScope, { }); + bool threw = false; + noteResolvedScope(codeBlock, vm, metadata, scope, ident, threw); + if (threw) [[unlikely]] + return { }; + + ASSERT(metadata.m_getPutInfo.resolveType() != ModuleVar); + return scope->getPropertySlot(globalObject, ident, [&] (bool found, PropertySlot& slot) -> JSValue { + if (!found) { + if (metadata.m_getPutInfo.resolveMode() == ThrowIfNotFound) { + throwException(globalObject, throwScope, createUndefinedVariableError(globalObject, ident)); + return { }; + } + return jsUndefined(); + } + + JSValue result = JSValue(); + if (scope->isGlobalLexicalEnvironment()) { + // When we can't statically prove we need a TDZ check, we must perform the check on the slow path. + result = slot.getValue(globalObject, ident); + if (result == jsTDZValue()) { + throwException(globalObject, throwScope, createTDZError(globalObject, ident.string())); + return { }; + } + } + + tryCacheGetFromScopeGlobal(globalObject, codeBlock, vm, bytecode, scope, slot, ident); + + if (!result) + return slot.getValue(globalObject, ident); + return result; + }); +} + ALWAYS_INLINE JSCellButterfly* trySpreadFast(JSGlobalObject* globalObject, JSCell* iterable) { if (isJSArray(iterable)) { diff --git a/Source/JavaScriptCore/runtime/FileBasedFuzzerAgent.cpp b/Source/JavaScriptCore/runtime/FileBasedFuzzerAgent.cpp index 7595afefa7e4..ea7900657b11 100644 --- a/Source/JavaScriptCore/runtime/FileBasedFuzzerAgent.cpp +++ b/Source/JavaScriptCore/runtime/FileBasedFuzzerAgent.cpp @@ -61,6 +61,7 @@ SpeculatedType FileBasedFuzzerAgent::getPredictionInternal(CodeBlock* codeBlock, // FIXME: the output of codeBlock->expressionInfoForBytecodeIndex() allows for some of // these opcodes to have predictions, but not all instances can be reliably targeted. case op_get_from_scope: // partially broken https://bugs.webkit.org/show_bug.cgi?id=203603 + case op_resolve_and_get_from_scope: case op_get_from_arguments: // partially broken https://bugs.webkit.org/show_bug.cgi?id=203608 case op_get_by_val: // partially broken https://bugs.webkit.org/show_bug.cgi?id=203665 case op_get_by_id: // sometimes occurs implicitly for things related to Symbol.iterator diff --git a/Source/JavaScriptCore/runtime/GetPutInfo.h b/Source/JavaScriptCore/runtime/GetPutInfo.h index 8233aa1f0b6f..54e0359559c5 100644 --- a/Source/JavaScriptCore/runtime/GetPutInfo.h +++ b/Source/JavaScriptCore/runtime/GetPutInfo.h @@ -232,7 +232,11 @@ class GetPutInfo { static constexpr unsigned initializationBits = ((1 << modeShift) - 1) & ~typeBits; static constexpr unsigned modeBits = ((1 << 30) - 1) & ~initializationBits & ~typeBits; static constexpr unsigned isStrictBit = 1 << 30; + // Only in the DFG's GetDynamicVar for resolve_and_get_from_scope: the scope it is given is the base scope, and the + // operation resolves the identifier through it first (one node, so no OSR exit can land between the two halves). + static constexpr unsigned resolvesScopeFirstBit = 1u << 31; static_assert((modeBits & initializationBits & typeBits & isStrictBit) == 0x0, "There should be no intersection between ResolveMode ResolveType and InitializationMode"); + static_assert((resolvesScopeFirstBit & (modeBits | initializationBits | typeBits | isStrictBit)) == 0x0); GetPutInfo() = default; diff --git a/Source/JavaScriptCore/runtime/PredictionFileCreatingFuzzerAgent.cpp b/Source/JavaScriptCore/runtime/PredictionFileCreatingFuzzerAgent.cpp index 88bf8a67b50c..c9c12a09963d 100644 --- a/Source/JavaScriptCore/runtime/PredictionFileCreatingFuzzerAgent.cpp +++ b/Source/JavaScriptCore/runtime/PredictionFileCreatingFuzzerAgent.cpp @@ -47,6 +47,7 @@ SpeculatedType PredictionFileCreatingFuzzerAgent::getPredictionInternal(CodeBloc case op_get_argument: case op_get_from_arguments: case op_get_from_scope: + case op_resolve_and_get_from_scope: case op_get_by_id: case op_get_length: case op_get_by_id_with_this: