diff --git a/Source/JavaScriptCore/debugger/Debugger.cpp b/Source/JavaScriptCore/debugger/Debugger.cpp index adca13a344290..d778b3c1546b6 100644 --- a/Source/JavaScriptCore/debugger/Debugger.cpp +++ b/Source/JavaScriptCore/debugger/Debugger.cpp @@ -239,8 +239,22 @@ void Debugger::detach(JSGlobalObject* globalObject, ReasonForDetach reason) globalObject->setDebugger(nullptr); - if (m_globalObjects.isEmpty()) + if (m_globalObjects.isEmpty()) { clearParsedData(); +#if USE(BUN_JSC_ADDITIONS) + // A pause started from a VM trap can be stepped out of into code that has no + // debug opcodes, so the step never completes and the frontend may disconnect + // (detaching us) while it is still armed. Nothing above runs in that case because + // we are not paused, so drop the stale state here: clearDebuggerRequests() has + // already reset the per-CodeBlock stepping flags, and a later attach() followed + // by setSteppingMode(SteppingModeEnabled) must not be a no-op. + m_currentCallFrame = nullptr; + resetImmediatePauseState(); + resetEventualPauseState(); + resetAsyncPauseState(); + m_steppingMode = SteppingModeDisabled; +#endif + } } bool Debugger::isAttached(JSGlobalObject* globalObject) @@ -969,11 +983,33 @@ void Debugger::continueProgram() m_doneProcessingDebuggerEvents = true; } +#if USE(BUN_JSC_ADDITIONS) +// breakProgram() can be entered from a VM trap while the paused frame is running code that +// was compiled before the debugger attached. Such a frame has no op_debug sites, so the +// atStatement / returnEvent hooks that complete a step-next/over/out targeted at it never +// run: the step would silently act as "continue" while leaving m_pauseOnCallFrame pointing at +// a frame that is going away. Pausing at the next opportunity instead (the behaviour of +// step-into) completes as soon as any code with debug opcodes runs and leaves nothing armed. +bool Debugger::currentCallFrameCanCompleteStep() const +{ + if (!m_currentCallFrame || m_currentCallFrame->isNativeCalleeFrame()) + return true; + CodeBlock* codeBlock = m_currentCallFrame->codeBlock(); + return !codeBlock || codeBlock->wasCompiledWithDebuggingOpcodes(); +} +#endif + void Debugger::stepNextExpression() { if (!m_isPaused) return; +#if USE(BUN_JSC_ADDITIONS) + if (!currentCallFrameCanCompleteStep()) { + stepIntoStatement(); + return; + } +#endif m_pauseOnCallFrame = m_currentCallFrame; m_pauseOnStepNext = true; setSteppingMode(SteppingModeEnabled); @@ -995,6 +1031,12 @@ void Debugger::stepOverStatement() if (!m_isPaused) return; +#if USE(BUN_JSC_ADDITIONS) + if (!currentCallFrameCanCompleteStep()) { + stepIntoStatement(); + return; + } +#endif m_pauseOnCallFrame = m_currentCallFrame; setSteppingMode(SteppingModeEnabled); m_doneProcessingDebuggerEvents = true; @@ -1005,6 +1047,12 @@ void Debugger::stepOutOfFunction() if (!m_isPaused) return; +#if USE(BUN_JSC_ADDITIONS) + if (!currentCallFrameCanCompleteStep()) { + stepIntoStatement(); + return; + } +#endif EntryFrame* topEntryFrame = m_vm.topEntryFrame; m_pauseOnCallFrame = m_currentCallFrame ? m_currentCallFrame->callerFrame(topEntryFrame) : nullptr; m_pauseOnStepOut = true; diff --git a/Source/JavaScriptCore/debugger/Debugger.h b/Source/JavaScriptCore/debugger/Debugger.h index c3e0315c30c7f..9770ee41677ad 100644 --- a/Source/JavaScriptCore/debugger/Debugger.h +++ b/Source/JavaScriptCore/debugger/Debugger.h @@ -315,6 +315,9 @@ class Debugger : public DoublyLinkedListNode { void resetImmediatePauseState(); void NODELETE resetEventualPauseState(); void resetAsyncPauseState(); +#if USE(BUN_JSC_ADDITIONS) + bool currentCallFrameCanCompleteStep() const; +#endif enum SteppingMode { SteppingModeDisabled, diff --git a/Source/JavaScriptCore/debugger/DebuggerCallFrame.cpp b/Source/JavaScriptCore/debugger/DebuggerCallFrame.cpp index d667c716e99b5..609597d947960 100644 --- a/Source/JavaScriptCore/debugger/DebuggerCallFrame.cpp +++ b/Source/JavaScriptCore/debugger/DebuggerCallFrame.cpp @@ -150,11 +150,21 @@ DebuggerScope* DebuggerCallFrame::scope(VM& vm) if (!m_scope) { JSScope* scope; +#if !USE(BUN_JSC_ADDITIONS) CodeBlock* codeBlock = m_validMachineFrame->isNativeCalleeFrame() ? nullptr : m_validMachineFrame->codeBlock(); +#endif if (isTailDeleted()) scope = m_shadowChickenFrame.scope; +#if USE(BUN_JSC_ADDITIONS) + // A pause entered through a VM trap can sit on frames whose scope register is not live + // (see CallFrame::scopeIfScopeRegisterIsLive); the callee's scope is the answer there, + // as it is for any frame of code compiled without debugging opcodes. + else if (JSScope* liveScope = m_validMachineFrame->scopeIfScopeRegisterIsLive()) + scope = liveScope; +#else else if (codeBlock && codeBlock->scopeRegister().isValid()) scope = m_validMachineFrame->scope(codeBlock->scopeRegister().offset()); +#endif else if (JSCallee* callee = dynamicDowncast(m_validMachineFrame->jsCallee())) scope = callee->scope(); else diff --git a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp index 704e5c6ab81ab..512f7d3e61d04 100644 --- a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp +++ b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp @@ -7436,7 +7436,21 @@ void ByteCodeParser::handleGetScope(VirtualRegister destination) void ByteCodeParser::handleCheckTraps() { +#if USE(BUN_JSC_ADDITIONS) + if (Options::usePollingTraps() || m_graph.m_plan.isUnlinked()) { + addToGraph(CheckTraps); + // With polling traps the debugger trap callback (VMTraps::handleDebuggerBreak) runs + // arbitrary JS from inside this frame and jettisons the code blocks on the stack before + // returning; this is where the frame then leaves, instead of carrying on with whatever + // it had hoisted above the loop. + if (!m_graph.m_plan.isUnlinked()) + addToGraph(InvalidationPoint); + return; + } + addToGraph(InvalidationPoint); +#else addToGraph((Options::usePollingTraps() || m_graph.m_plan.isUnlinked()) ? CheckTraps : InvalidationPoint); +#endif } void ByteCodeParser::emitPutById( diff --git a/Source/JavaScriptCore/inspector/JSGlobalObjectInspectorController.cpp b/Source/JavaScriptCore/inspector/JSGlobalObjectInspectorController.cpp index 803c4e3acbbc8..331df20b8b7ab 100644 --- a/Source/JavaScriptCore/inspector/JSGlobalObjectInspectorController.cpp +++ b/Source/JavaScriptCore/inspector/JSGlobalObjectInspectorController.cpp @@ -149,9 +149,6 @@ void JSGlobalObjectInspectorController::connectFrontend(FrontendChannel& fronten void JSGlobalObjectInspectorController::disconnectFrontend(FrontendChannel& frontendChannel) { - // FIXME: change this to notify agents which frontend has disconnected (by id). - m_agents.willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); - m_frontendRouter->disconnectFrontend(frontendChannel); m_isAutomaticInspection = false; @@ -161,6 +158,9 @@ void JSGlobalObjectInspectorController::disconnectFrontend(FrontendChannel& fron if (!disconnectedLastFrontend) return; + // FIXME: change this to notify agents which frontend has disconnected (by id). + m_agents.willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); + #if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS) if (m_augmentingClient) m_augmentingClient->inspectorDisconnected(); diff --git a/Source/JavaScriptCore/interpreter/CallFrame.cpp b/Source/JavaScriptCore/interpreter/CallFrame.cpp index 6a870c752969a..2391396055f65 100644 --- a/Source/JavaScriptCore/interpreter/CallFrame.cpp +++ b/Source/JavaScriptCore/interpreter/CallFrame.cpp @@ -158,6 +158,25 @@ CodeOrigin CallFrame::codeOrigin() const return CodeOrigin(callSiteIndex().bytecodeIndex()); } +#if USE(BUN_JSC_ADDITIONS) +JSScope* CallFrame::scopeIfScopeRegisterIsLive() const +{ + // BytecodeUseDef keeps the scope register live only in code compiled with debugging + // opcodes, and only after op_enter. Outside of that the slot holds whatever happened to + // be there: the DFG does not allocate it, a baseline frame reached by OSR exit from such + // code has a dead value in it, and a VM trap serviced in the prologue sees a frame whose + // locals have not been written at all. Between op_enter and op_get_scope it is undefined. + if (isNativeCalleeFrame()) + return nullptr; + CodeBlock* codeBlock = this->codeBlock(); + if (!codeBlock || !codeBlock->wasCompiledWithDebuggingOpcodes() || !codeBlock->scopeRegister().isValid()) + return nullptr; + if (!bytecodeIndex().offset()) + return nullptr; + return dynamicDowncast(registers()[codeBlock->scopeRegister().offset()].jsValue()); +} +#endif + Register* CallFrame::topOfFrameInternal() { CodeBlock* codeBlock = this->codeBlock(); diff --git a/Source/JavaScriptCore/interpreter/CallFrame.h b/Source/JavaScriptCore/interpreter/CallFrame.h index 553b435723fdd..69eabecc03a72 100644 --- a/Source/JavaScriptCore/interpreter/CallFrame.h +++ b/Source/JavaScriptCore/interpreter/CallFrame.h @@ -269,6 +269,12 @@ using JSInstruction = BaseInstruction; // CodeOrigin(BytecodeIndex(0)) if we're in native code. JS_EXPORT_PRIVATE CodeOrigin codeOrigin() const; +#if USE(BUN_JSC_ADDITIONS) + // The scope held in this frame's scope register, or null whenever that slot cannot be + // trusted; callers then fall back to the callee's scope. See CallFrame.cpp. + JSScope* scopeIfScopeRegisterIsLive() const; +#endif + inline Register* topOfFrame(); const JSInstruction* NODELETE currentVPC() const; // This only makes sense in the LLInt and baseline. diff --git a/Source/JavaScriptCore/interpreter/ShadowChicken.cpp b/Source/JavaScriptCore/interpreter/ShadowChicken.cpp index 9c22b466731de..3ff2ac04ba57b 100644 --- a/Source/JavaScriptCore/interpreter/ShadowChicken.cpp +++ b/Source/JavaScriptCore/interpreter/ShadowChicken.cpp @@ -334,6 +334,13 @@ void ShadowChicken::update(VM& vm, CallFrame* callFrame) bool isTailDeleted = false; JSScope* scope = nullptr; CodeBlock* codeBlock = callFrame->isNativeCalleeFrame() ? nullptr : callFrame->codeBlock(); +#if USE(BUN_JSC_ADDITIONS) + // Same rule as DebuggerCallFrame::scope(): this walk can now also happen from a + // debugger pause entered through a VM trap, so the slot is only read when it is live. + if (JSScope* liveScope = callFrame->scopeIfScopeRegisterIsLive()) + scope = liveScope; + else if (foundFrame) { +#else JSValue scopeValue = callFrame->bytecodeIndex() && codeBlock && codeBlock->scopeRegister().isValid() ? callFrame->registers()[codeBlock->scopeRegister().offset()].jsValue() : jsUndefined(); @@ -341,6 +348,7 @@ void ShadowChicken::update(VM& vm, CallFrame* callFrame) scope = uncheckedDowncast(scopeValue.asCell()); RELEASE_ASSERT(scope->inherits()); } else if (foundFrame) { +#endif scope = m_log[indexInLog].scope; if (scope) RELEASE_ASSERT(scope->inherits()); diff --git a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp index a2de887497b0a..0484d368e4cae 100644 --- a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp +++ b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp @@ -560,7 +560,14 @@ UGPRPair SYSV_ABI llint_check_stack_and_vm_traps(CallFrame* callFrame, const JSI #endif } - if (vm.traps().handleTrapsIfNeeded()) { +#if USE(BUN_JSC_ADDITIONS) + // This frame is at bytecode 0 with op_enter still to run, so it cannot be paused on; the + // debugger trap callback (VM::setDebuggerTrapCallback) waits for the next op_check_traps. + bool handledTraps = vm.traps().handleTrapsIfNeeded(VMTraps::NonDebuggerAsyncEvents); +#else + bool handledTraps = vm.traps().handleTrapsIfNeeded(); +#endif + if (handledTraps) { if (vm.hasPendingTerminationException()) { throwScope.release(); callFrame->convertToZombieFrame(vm, codeBlock); diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 575a5ec3d8246..9cbc90fa98dc7 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -1103,6 +1103,30 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { JS_EXPORT_PRIVATE bool hasExceptionsAfterHandlingTraps(); CONCURRENT_SAFE void notifyNeedDebuggerBreak() { traps().fireTrap(VMTraps::NeedDebuggerBreak); } +#if USE(BUN_JSC_ADDITIONS) + // Lets an embedder service NeedDebuggerBreak itself, e.g. to attach a debugger to a program + // that is already running and enter Debugger::breakProgram() from code that was compiled + // without op_debug sites. VMTraps::handleDebuggerBreak() calls it on the owning thread with + // the API lock held, with no exception pending, and it must return the same way; it may + // run JS and pause. It is never nested: a NeedDebuggerBreak fired while it runs is taken + // again once it returns. Termination requested meanwhile is deferred and re-fired after it. + // + // Where it runs: op_check_traps (loop headers, every tier), or wherever the embedder calls + // traps().handleTrapsIfNeeded(VMTraps::NeedDebuggerBreak) itself. The LLInt and IPInt + // function prologues deliberately do not service this bit, because they run before the + // callee's frame is initialised. Nothing else retires the bit either: an embedder that + // keeps the API lock while idle has to make that call from its idle loop, otherwise the + // signal sender keeps interrupting the thread until some loop happens to run. + // + // What it buys: pausing and evaluating in code that was already running. Breakpoints, + // `debugger` statements and stepping only take effect in code compiled after the debugger + // attached (CodeGenerationMode::Debugger), i.e. once the currently running code has been + // re-entered after the VM went idle; Debugger degrades steps out of such frames to a pause + // at the next opportunity. + using DebuggerTrapCallback = void (*)(VM&); + CONCURRENT_SAFE void setDebuggerTrapCallback(DebuggerTrapCallback cb) { m_debuggerTrapCallback.store(cb, std::memory_order_release); } + DebuggerTrapCallback debuggerTrapCallback() const { return m_debuggerTrapCallback.load(std::memory_order_acquire); } +#endif CONCURRENT_SAFE void notifyNeedShellTimeoutCheck() { traps().fireTrap(VMTraps::NeedShellTimeoutCheck); } CONCURRENT_SAFE void notifyNeedTermination() { traps().fireTrap(VMTraps::NeedTermination); } CONCURRENT_SAFE void notifyNeedWatchdogCheck() { traps().fireTrap(VMTraps::NeedWatchdogCheck); } @@ -1360,6 +1384,9 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { const Ref m_syncWaiter; std::atomic m_numberOfActiveJITPlans { 0 }; +#if USE(BUN_JSC_ADDITIONS) + std::atomic m_debuggerTrapCallback { nullptr }; +#endif Vector> m_didPopListeners; diff --git a/Source/JavaScriptCore/runtime/VMTraps.cpp b/Source/JavaScriptCore/runtime/VMTraps.cpp index 7dd488259b83c..de8e06c88f7b9 100644 --- a/Source/JavaScriptCore/runtime/VMTraps.cpp +++ b/Source/JavaScriptCore/runtime/VMTraps.cpp @@ -30,6 +30,7 @@ #include "CodeBlock.h" #include "CodeBlockSet.h" #include "DFGCommonData.h" +#include "DeferTermination.h" #include "ExceptionHelpers.h" #include "HeapInlines.h" #include "JSCJSValueInlines.h" @@ -45,6 +46,7 @@ #include #include #include +#include #include #include @@ -166,6 +168,10 @@ void VMTraps::tryInstallTrapBreakpoints(VMTraps::SignalContext& context, StackBo } } +#endif // ENABLE(SIGNAL_BASED_VM_TRAPS) + +#if ENABLE(SIGNAL_BASED_VM_TRAPS) || USE(BUN_JSC_ADDITIONS) + void VMTraps::invalidateCodeBlocksOnStack() { invalidateCodeBlocksOnStack(vm().topCallFrame); @@ -177,13 +183,17 @@ void VMTraps::invalidateCodeBlocksOnStack(CallFrame* topCallFrame) invalidateCodeBlocksOnStack(codeBlockSetLocker, topCallFrame); } -void VMTraps::invalidateCodeBlocksOnStack(Locker&, CallFrame* topCallFrame) +void VMTraps::invalidateCodeBlocksOnStack(Locker& codeBlockSetLocker, CallFrame* topCallFrame) { if (!m_needToInvalidateCodeBlocks) return; m_needToInvalidateCodeBlocks = false; + jettisonOptimizedCodeBlocksOnStack(codeBlockSetLocker, topCallFrame); +} +void VMTraps::jettisonOptimizedCodeBlocksOnStack(Locker&, CallFrame* topCallFrame) +{ EntryFrame* entryFrame = vm().topEntryFrame; CallFrame* callFrame = topCallFrame; @@ -198,6 +208,10 @@ void VMTraps::invalidateCodeBlocksOnStack(Locker&, CallFrame* topCallFrame } } +#endif // ENABLE(SIGNAL_BASED_VM_TRAPS) || USE(BUN_JSC_ADDITIONS) + +#if ENABLE(SIGNAL_BASED_VM_TRAPS) + class VMTraps::SignalSender final : public ThreadSafeRefCounted { public: SignalSender(const AbstractLocker&, VM& vm) @@ -450,6 +464,14 @@ bool VMTraps::handleTraps(VMTraps::BitField mask) if (isDeferringTermination()) mask &= ~NeedTermination; +#if USE(BUN_JSC_ADDITIONS) + // JS run by the debugger trap callback polls the traps too. Leave the bit alone while the + // callback is on the stack; the handleTraps() loop that invoked it picks the re-fire up once + // it returns, so re-fires coalesce instead of nesting one callback inside another. + if (m_isHandlingDebuggerBreak) + mask &= ~NeedDebuggerBreak; +#endif + { Locker codeBlockSetLocker { vm.heap.codeBlockSet().getLock() }; vm.heap.forEachCodeBlockIgnoringJITPlans(codeBlockSetLocker, [&] (CodeBlock* codeBlock) { @@ -483,7 +505,11 @@ bool VMTraps::handleTraps(VMTraps::BitField mask) auto event = takeTopPriorityTrap(mask); switch (event) { case NeedDebuggerBreak: +#if USE(BUN_JSC_ADDITIONS) + handleDebuggerBreak(); +#else invalidateCodeBlocksOnStack(vm.topCallFrame); +#endif didHandleTrap = true; break; @@ -523,6 +549,43 @@ bool VMTraps::handleTraps(VMTraps::BitField mask) RELEASE_AND_RETURN(scope, didHandleTrap); } +#if USE(BUN_JSC_ADDITIONS) +void VMTraps::handleDebuggerBreak() +{ + VM& vm = this->vm(); + auto callback = vm.debuggerTrapCallback(); + if (!callback) { + invalidateCodeBlocksOnStack(vm.topCallFrame); + return; + } + + // With signal based traps the frame that was running optimized code has already left it + // through the trap breakpoints by the time we get here, and any optimized frames below it + // are parked at calls, so the callback may re-enter the VM without jettisoning the rest of + // the stack; doing so on every delivery would deoptimize the program once per CDP batch. + // With polling traps the caller can be a DFG/FTL CheckTraps site inside a live frame. That + // frame must not continue past the poll with state it computed before the callback ran, so + // jettison the stack unconditionally and let the InvalidationPoint the DFG emits after + // CheckTraps (see ByteCodeParser::handleCheckTraps) exit to baseline on return. + if (Options::usePollingTraps()) { + Locker codeBlockSetLocker { vm.heap.codeBlockSet().getLock() }; + m_needToInvalidateCodeBlocks = false; + jettisonOptimizedCodeBlocksOnStack(codeBlockSetLocker, vm.topCallFrame); + } else + m_needToInvalidateCodeBlocks = false; + + auto scope = DECLARE_THROW_SCOPE(vm); + { + SetForScope handling(m_isHandlingDebuggerBreak, true); + // A termination that arrives while the debugger is being serviced is re-fired as a trap + // when this scope ends and taken by the handleTraps() loop that called us. + DeferTerminationForAWhile deferTermination(vm); + callback(vm); + } + scope.releaseAssertNoExceptionExceptTermination(); +} +#endif + bool VMTraps::handleTrapsIfNeeded(VMTraps::BitField mask) { if (needHandling(mask)) diff --git a/Source/JavaScriptCore/runtime/VMTraps.h b/Source/JavaScriptCore/runtime/VMTraps.h index d84d871e31853..7acc5361c96de 100644 --- a/Source/JavaScriptCore/runtime/VMTraps.h +++ b/Source/JavaScriptCore/runtime/VMTraps.h @@ -302,25 +302,35 @@ class VMTraps { JS_EXPORT_PRIVATE void deferTerminationSlow(DeferAction); JS_EXPORT_PRIVATE void undoDeferTerminationSlow(DeferAction); -#if ENABLE(SIGNAL_BASED_VM_TRAPS) - class SignalSender; - friend class SignalSender; - +#if ENABLE(SIGNAL_BASED_VM_TRAPS) || USE(BUN_JSC_ADDITIONS) void invalidateCodeBlocksOnStack(); void invalidateCodeBlocksOnStack(CallFrame* topCallFrame); void invalidateCodeBlocksOnStack(Locker& codeBlockSetLocker, CallFrame* topCallFrame); - - void addSignalSender(SignalSender*); - void removeSignalSender(SignalSender*); + void jettisonOptimizedCodeBlocksOnStack(Locker& codeBlockSetLocker, CallFrame* topCallFrame); #else void invalidateCodeBlocksOnStack() { } void invalidateCodeBlocksOnStack(CallFrame*) { } #endif +#if ENABLE(SIGNAL_BASED_VM_TRAPS) + class SignalSender; + friend class SignalSender; + + void addSignalSender(SignalSender*); + void removeSignalSender(SignalSender*); +#endif + +#if USE(BUN_JSC_ADDITIONS) + void handleDebuggerBreak(); +#endif + StackManager m_stack; Atomic m_trapBits { 0 }; unsigned m_deferTerminationCount { 0 }; bool m_needToInvalidateCodeBlocks { false }; +#if USE(BUN_JSC_ADDITIONS) + bool m_isHandlingDebuggerBreak { false }; +#endif bool m_isShuttingDown { false }; bool m_suspendedTerminationException { false }; bool m_threadStopRequested { false }; diff --git a/Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp b/Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp index 8c2b25daf4057..59b02042bda81 100644 --- a/Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp +++ b/Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp @@ -1454,7 +1454,14 @@ WASM_IPINT_EXTERN_CPP_DECL(check_stack_and_vm_traps, void* candidateNewStackPoin UNUSED_PARAM(callFrame); #endif - if (vm.traps().handleTrapsIfNeeded()) { +#if USE(BUN_JSC_ADDITIONS) + // Nothing stores vm.topCallFrame on the way here, so the debugger trap callback + // (VM::setDebuggerTrapCallback) would walk a stale stack; it waits for a JS op_check_traps. + bool handledTraps = vm.traps().handleTrapsIfNeeded(VMTraps::NonDebuggerAsyncEvents); +#else + bool handledTraps = vm.traps().handleTrapsIfNeeded(); +#endif + if (handledTraps) { if (vm.hasPendingTerminationException()) IPINT_THROW(Wasm::ExceptionType::Termination); ASSERT(!vm.exceptionForInspection());