diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/SafepointALotCurrentFramesTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/SafepointALotCurrentFramesTest.java new file mode 100644 index 0000000000..7f6e515ae6 --- /dev/null +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/SafepointALotCurrentFramesTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.graal.python.test; + +import java.io.ByteArrayOutputStream; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.junit.Assert; +import org.junit.Test; + +public class SafepointALotCurrentFramesTest { + + @Test + public void currentFramesWhileThreadsExecute() { + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + try (Engine engine = Engine.newBuilder("python").allowExperimentalOptions(true).// + option("engine.SafepointALot", "true").build(); + Context context = Context.newBuilder("python").engine(engine).allowExperimentalOptions(true).allowAllAccess(true).err(errorOutput).build()) { + context.eval("python", """ + import sys + import threading + import time + from contextlib import nullcontext + + worker_count = 16 + run_for = 10.0 + start = threading.Event() + stop = threading.Event() + errors = [] + + def leaf(value): + return value + 1 + + def generated_values(value): + for offset in range(4): + yield leaf(value + offset) + + def work(): + with nullcontext(): + values = [item * 2 for item in generated_values(0)] + return {item: item + 1 for item in values} + + def record_error(error): + errors.append((type(error).__name__, str(error))) + + def thread_exception(args): + record_error(args.exc_value) + + threading.excepthook = thread_exception + + def watch_frames(): + try: + start.wait() + while not stop.is_set(): + sys._current_frames() + except BaseException as error: + record_error(error) + + def worker(): + try: + start.wait() + while not stop.is_set(): + work() + except BaseException as error: + record_error(error) + + watcher = threading.Thread(target=watch_frames) + workers = [threading.Thread(target=worker) for _ in range(worker_count)] + watcher.start() + for thread in workers: + thread.start() + start.set() + try: + time.sleep(run_for) + finally: + stop.set() + watcher.join() + for thread in workers: + thread.join() + + if errors: + raise AssertionError(errors) + """); + } + assertNoUnexpectedErrors(errorOutput); + } + + @Test + public void weakrefCallbacksWhileMainThreadExecutes() { + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + try (Engine engine = Engine.newBuilder("python").allowExperimentalOptions(true).// + option("engine.SafepointALot", "true").build(); + Context context = Context.newBuilder("python").engine(engine).allowExperimentalOptions(true).allowAllAccess(true).err(errorOutput).build()) { + context.eval("python", """ + import sys + import gc + import time + import threading + import weakref + from contextlib import nullcontext + + run_for = 10.0 + start = threading.Event() + stop = threading.Event() + errors = [] + live_refs = {} + shared_list = [] + shared_dict = {"main": 0, "callback": 0} + callback_count = 0 + + def leaf(value): + return value + 1 + + def generated_values(value): + for offset in range(4): + yield leaf(value + offset) + + def work(role): + with nullcontext(): + values = [item * 2 for item in generated_values(0)] + shared_list.extend(values) + if len(shared_list) > 256: + del shared_list[:128] + shared_dict[role] = shared_dict[role] + 1 + return {item: item + 1 for item in values} + + def record_error(error): + errors.append((type(error).__name__, str(error))) + + def thread_exception(args): + record_error(args.exc_value) + + threading.excepthook = thread_exception + + class Target: + pass + + def weakref_callback(reference): + global callback_count + try: + live_refs.pop(id(reference), None) + callback_count += 1 + frame = sys._getframe() + while frame is not None: + frame = frame.f_back + work("callback") + except BaseException as error: + record_error(error) + + def submit_callbacks(): + try: + start.wait() + while not stop.is_set(): + for _ in range(128): + target = Target() + reference = weakref.ref(target, weakref_callback) + live_refs[id(reference)] = reference + gc.collect() + except BaseException as error: + record_error(error) + + submitter = threading.Thread(target=submit_callbacks) + submitter.start() + start.set() + deadline = time.monotonic() + run_for + try: + while time.monotonic() < deadline: + work("main") + finally: + stop.set() + submitter.join() + for _ in range(8): + gc.collect() + time.sleep(0.01) + + if errors: + raise AssertionError(errors) + if callback_count == 0: + raise AssertionError("weakref callbacks were not submitted") + if shared_dict["main"] == 0 or shared_dict["callback"] == 0 or not shared_list: + raise AssertionError("shared data structures were not mutated") + """); + } + assertNoUnexpectedErrors(errorOutput); + } + + private static void assertNoUnexpectedErrors(ByteArrayOutputStream errorOutput) { + Assert.assertEquals("unexpected output in the context error stream", "", errorOutput.toString()); + } +} diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/UnavailableFrameLocationTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/UnavailableFrameLocationTest.java new file mode 100644 index 0000000000..d3c8ce7bc9 --- /dev/null +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/UnavailableFrameLocationTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.graal.python.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyExecutable; +import org.junit.Test; + +import com.oracle.graal.python.PythonLanguage; +import com.oracle.graal.python.builtins.objects.frame.PFrame; +import com.oracle.graal.python.nodes.frame.GetFrameLocalsNode; +import com.oracle.graal.python.nodes.frame.MaterializeFrameNode; +import com.oracle.graal.python.nodes.frame.ReadFrameNode; +import com.oracle.graal.python.runtime.CallerFlags; +import com.oracle.truffle.api.bytecode.BytecodeFrame; +import com.oracle.truffle.api.frame.FrameInstance.FrameAccess; +import com.oracle.truffle.api.nodes.Node; + +public class UnavailableFrameLocationTest { + + @Test + public void unavailableLocals() { + checkFrame(false, false); + } + + @Test + public void unavailableGeneratorLocals() { + checkFrame(true, false); + } + + @Test + public void preserveCapturedLocals() { + checkFrame(false, true); + } + + private static void checkFrame(boolean generator, boolean captureFirst) { + try (Context context = Context.newBuilder("python").allowAllAccess(true).build()) { + Value checkLocals = context.eval("python", """ + def check_locals(frame, captured): + proxy = frame.f_locals + if captured: + assert proxy['local_value'] == 42 + else: + assert len(proxy) == 0 + assert list(proxy) == [] + assert proxy.copy() == {} + assert 'local_value' not in proxy + proxy['extra'] = 123 + assert proxy['extra'] == 123 + assert 'extra' in proxy + assert proxy.copy()['extra'] == 123 + del proxy['extra'] + assert 'extra' not in proxy + check_locals + """); + context.getBindings("python").putMember("capture", (ProxyExecutable) args -> { + // This allows us to manually trigger stack walk and process the result using MaterializeFrameNode + // passing various combinations of arguments to it + + ReadFrameNode.StackWalkResult result = ReadFrameNode.getFrame(null, null, FrameAccess.MATERIALIZE, + ReadFrameNode.AllPythonFramesSelector.INSTANCE, 0, CallerFlags.ALL_FRAME_FLAGS); + assertNotNull(result); + MaterializeFrameNode materialize = MaterializeFrameNode.getUncached(); + assertThrows(AssertionError.class, () -> materialize.execute(null, false, false, result.frame())); + BytecodeFrame captured = null; + if (captureFirst) { + captured = materialize.execute(result.callNode(), true, true, result.frame()).getBytecodeFrame(); + assertNotNull(captured); + } + Node unavailable = PythonLanguage.get(null).unavailableSafepointLocation; + PFrame pyFrame = materialize.execute(unavailable, true, true, result.frame()); + assertNull(pyFrame.getBytecodeNode()); + assertEquals(-1, pyFrame.getBci()); + assertEquals(-1, pyFrame.getLine()); + assertSame(captured, pyFrame.getBytecodeFrame()); + assertFalse(pyFrame.syncsLocals()); + assertFalse(pyFrame.outdatedCallerFlags(CallerFlags.NEEDS_MATERIALIZED_LOCALS)); + assertNotNull(GetFrameLocalsNode.executeUncached(pyFrame, true)); + checkLocals.execute(context.asValue(pyFrame), captureFirst); + + // A later observation with a valid location must resume normal locals capture. + assertSame(pyFrame, materialize.execute(result.callNode(), true, true, result.frame())); + assertNotNull(pyFrame.getBytecodeNode()); + assertNotNull(pyFrame.getBytecodeFrame()); + assertTrue(pyFrame.hasMaterializedFrame() || pyFrame.syncsLocals()); + return null; + }); + context.eval("python", generator ? """ + def target(): + local_value = 42 + capture() + yield local_value + assert next(target()) == 42 + """ : """ + def target(): + local_value = 42 + capture() + target() + """); + } + } +} diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/FrameLocalsProxyBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/FrameLocalsProxyBuiltins.java index 0f551cc7f2..69a92ef1cb 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/FrameLocalsProxyBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/FrameLocalsProxyBuiltins.java @@ -104,6 +104,9 @@ protected List> getNodeFa private static int findSlot(VirtualFrame frame, Node inliningTarget, PFrameLocalsProxy self, Object key, PyObjectRichCompareBool equals) { BytecodeFrame bytecodeFrame = self.getBytecodeFrame(); + if (bytecodeFrame == null) { + return -1; + } BytecodeDSLFrameInfo info = (BytecodeDSLFrameInfo) bytecodeFrame.getFrameDescriptorInfo(); // Cell parameters occur in both varnames and cellvars. The function prologue moves their // value to the cell slot and clears the regular slot, so prefer the later cell slot. @@ -158,8 +161,9 @@ private static PDict extras(PFrameLocalsProxy self, PythonLanguage language) { private static PDict snapshot(PFrameLocalsProxy self, PythonLanguage language, Node inliningTarget, HashingStorageSetItem setItem) { PDict result = PFactory.createDict(language); - BytecodeDSLFrameInfo info = (BytecodeDSLFrameInfo) self.getBytecodeFrame().getFrameDescriptorInfo(); - for (int i = 0; i < info.getVariableCount(); i++) { + BytecodeFrame bytecodeFrame = self.getBytecodeFrame(); + BytecodeDSLFrameInfo info = bytecodeFrame == null ? null : (BytecodeDSLFrameInfo) bytecodeFrame.getFrameDescriptorInfo(); + for (int i = 0; info != null && i < info.getVariableCount(); i++) { Object value = getSlotValue(self, i); if (value != null) { result.setDictStorage(setItem.execute(inliningTarget, result.getDictStorage(), info.getVariableName(i), value)); @@ -281,8 +285,9 @@ static int len(PFrameLocalsProxy self, private static int count(PFrameLocalsProxy self, Node inliningTarget, HashingStorageLen storageLen) { int count = 0; - BytecodeDSLFrameInfo info = (BytecodeDSLFrameInfo) self.getBytecodeFrame().getFrameDescriptorInfo(); - for (int i = 0; i < info.getVariableCount(); i++) { + BytecodeFrame bytecodeFrame = self.getBytecodeFrame(); + BytecodeDSLFrameInfo info = bytecodeFrame == null ? null : (BytecodeDSLFrameInfo) bytecodeFrame.getFrameDescriptorInfo(); + for (int i = 0; info != null && i < info.getVariableCount(); i++) { if (getSlotValue(self, i) != null) { count++; } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/PFrame.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/PFrame.java index 1586d626b3..7e8982b071 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/PFrame.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/PFrame.java @@ -232,7 +232,8 @@ public PFrame(PythonLanguage lang, @SuppressWarnings("unused") long threadState, /** * Get the bytecode frame with locals backing this frame. May be copied from the real frame or it might be materialized, - * depending on how the PFrame was synced. May be null when using custom locals. In most + * depending on how the PFrame was synced. May be null when using custom locals or when a + * safepoint could not recover the executing BytecodeNode. In most * cases, you should use {@link GetFrameLocalsNode} to get a copy of the locals instead of this method. */ public BytecodeFrame getBytecodeFrame() { @@ -294,7 +295,9 @@ public boolean hasMaterializedFrame() { } public boolean syncsLocals() { - return customLocals == null && !materializedFrame; + // An unavailable safepoint location cannot provide locals. A later materialization with + // a valid BytecodeNode restores synchronization. + return customLocals == null && !materializedFrame && bytecodeNode != null; } public PFrame.Reference getRef() { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/GetFrameLocalsNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/GetFrameLocalsNode.java index ce3df5c89c..3c2f86889a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/GetFrameLocalsNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/GetFrameLocalsNode.java @@ -124,6 +124,9 @@ PDict doIt(BytecodeFrame locals, @Cached InlinedIntValueProfile regularVarCountProfile, @Cached HashingStorageSetItem setItem) { HashingStorage storage = new DynamicObjectStorage(language); + if (locals == null) { + return PFactory.createDict(language, storage); + } BytecodeDSLFrameInfo info = (BytecodeDSLFrameInfo) locals.getFrameDescriptorInfo(); int regularVarCount = regularVarCountProfile.profile(inliningTarget, info.getRegularVariableCount()); int varCount = varCountProfile.profile(inliningTarget, info.getVariableCount()); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/MaterializeFrameNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/MaterializeFrameNode.java index 081687de5d..baac401f35 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/MaterializeFrameNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/MaterializeFrameNode.java @@ -127,12 +127,20 @@ public final PFrame executeOnStack(boolean markAsEscaped, boolean forceSync, Fra * {@code continueAt} method. We must use the on-stack BytecodeNode to resolve the * BCI that we read from its stack frame. For a frame that is on top of the stack, * this must be some adopted node in the AST that is currently being executed. + * The explicit {@link PythonLanguage#unavailableSafepointLocation} marker permits + * materialization without bytecode information when an async action has no usable + * location. */ public final PFrame execute(Node location, boolean markAsEscaped, boolean forceSync, Frame frameToMaterialize) { assert frameToMaterialize.getArguments().length != 2 : "caller forgot to unwrap continuation frame"; assert !(location instanceof PBytecodeDSLRootNode) : String.format("Materialized frame: location must not be PBytecodeDSLRootNode, was: %s", location); BytecodeNode bytecodeNode = BytecodeNode.get(location); + // A root-level safepoint can run after the bytecode interpreter has returned. Expect the + // explicit unavailable location in that case, but tolerate missing bytecode information + // from other locations as well when assertions are disabled. + assert bytecodeNode != null || !(PArguments.getCurrentFrameInfo(frameToMaterialize).getRootNode() instanceof PBytecodeDSLRootNode) || + location == PythonLanguage.get(null).unavailableSafepointLocation : "Missing BytecodeNode when materializing a Python frame"; return executeImpl(bytecodeNode, markAsEscaped, forceSync, frameToMaterialize); } @@ -156,10 +164,16 @@ static PFrame freshPFrameCustomLocals(BytecodeNode bytecodeNode, boolean markAsE } @Specialization(guards = {"getPFrame(frameToMaterialize) == null", "isGeneratorFrame(frameToMaterialize)"}) - static PFrame freshPFrameForGenerator(BytecodeNode bytecodeNode, @SuppressWarnings("unused") boolean markAsEscaped, @SuppressWarnings("unused") boolean forceSync, Frame frameToMaterialize) { + static PFrame freshPFrameForGenerator(BytecodeNode bytecodeNode, boolean markAsEscaped, @SuppressWarnings("unused") boolean forceSync, Frame frameToMaterialize, + @Bind PythonLanguage language) { MaterializedFrame generatorFrame = PGenerator.getGeneratorFrame(frameToMaterialize); PFrame.Reference frameRef = PArguments.getCurrentFrameInfo(frameToMaterialize); - PFrame escapedFrame = materializeGeneratorFrame(bytecodeNode, generatorFrame, PArguments.getFunctionObject(frameToMaterialize), PArguments.getGlobals(frameToMaterialize), frameRef); + PFrame escapedFrame; + if (bytecodeNode != null) { + escapedFrame = materializeGeneratorFrame(bytecodeNode, generatorFrame, PArguments.getFunctionObject(frameToMaterialize), PArguments.getGlobals(frameToMaterialize), frameRef); + } else { + escapedFrame = PFactory.createPFrame(language, frameRef, null, PArguments.getFunctionObject(frameToMaterialize), null); + } return doEscapeFrame(frameToMaterialize, escapedFrame, markAsEscaped, false, bytecodeNode, null); } @@ -167,6 +181,8 @@ static PFrame freshPFrameForGenerator(BytecodeNode bytecodeNode, @SuppressWarnin static PFrame alreadyEscapedFrame(BytecodeNode bytecodeNode, boolean markAsEscaped, boolean forceSync, Frame frameToMaterialize, @Shared("syncValuesNode") @Cached SyncFrameValuesNode syncValuesNode) { PFrame pyFrame = getPFrame(frameToMaterialize); + // Restore the node before syncsLocals() is checked if an earlier safepoint had no location. + pyFrame.setBytecodeNode(bytecodeNode); pyFrame.setLastCallerFlags(getCallerFlags(forceSync)); if (forceSync) { syncValuesNode.execute(pyFrame, frameToMaterialize, bytecodeNode); @@ -261,7 +277,9 @@ static void doSync(PFrame pyFrame, Frame frameToSync, BytecodeNode bytecodeNode) @Specialization(guards = {"pyFrame.syncsLocals()", "isGeneratorFrame(frameToSync)"}) static void doGenerator(PFrame pyFrame, Frame frameToSync, BytecodeNode bytecodeNode) { - pyFrame.setBytecodeFrame(bytecodeNode.createMaterializedFrame(0, (MaterializedFrame) frameToSync), true); + if (bytecodeNode != null) { + pyFrame.setBytecodeFrame(bytecodeNode.createMaterializedFrame(0, (MaterializedFrame) frameToSync), true); + } } @Specialization(guards = "!pyFrame.syncsLocals()") diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadFrameNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadFrameNode.java index 85fa12e5fa..492844c98f 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadFrameNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadFrameNode.java @@ -306,7 +306,7 @@ public static PFrame readFrameInThreadLocal(Access access, Reference startFrameI public static PFrame readFrameInThreadLocal(Access access, Reference startFrameInfo, FrameAccess frameAccess, FrameSelector selector, int level, int callerFlags, MaterializeFrameNode materializeFrameNode, boolean forceEscape) { Node location = access.getLocation(); - if (location instanceof PBytecodeDSLRootNode) { + if (location instanceof RootNode root && PBytecodeDSLRootNode.cast(root) != null) { // See AsyncPythonAction#execute for explanation location = PythonLanguage.get(null).unavailableSafepointLocation; } @@ -340,8 +340,8 @@ private static PFrame processStackWalkResult(MaterializeFrameNode materializeFra if (callerFrameResult != null) { Node location = getMaterializationLocation(callerFrameResult); PFrame pFrame = materializeFrameNode.execute(location, false, CallerFlags.needsLocals(callerFlags) && !CallerFlags.needsMaterializedLocals(callerFlags), callerFrameResult.frame); - if (CallerFlags.needsMaterializedLocals(callerFlags)) { - BytecodeNode bytecodeNode = pFrame.getBytecodeNode(); + BytecodeNode bytecodeNode = pFrame.getBytecodeNode(); + if (CallerFlags.needsMaterializedLocals(callerFlags) && bytecodeNode != null) { pFrame.setBytecodeFrame(bytecodeNode.createMaterializedFrame(0, (MaterializedFrame) callerFrameResult.frame), true); } return pFrame; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/AsyncHandler.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/AsyncHandler.java index d49fbf264a..ea9ca5f405 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/AsyncHandler.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/AsyncHandler.java @@ -59,6 +59,7 @@ import org.graalvm.polyglot.SandboxPolicy; import com.oracle.graal.python.PythonLanguage; +import com.oracle.graal.python.builtins.objects.frame.PFrame; import com.oracle.graal.python.builtins.objects.function.PArguments; import com.oracle.graal.python.builtins.objects.function.Signature; import com.oracle.graal.python.nodes.PRootNode; @@ -79,6 +80,8 @@ import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.debug.Debugger; +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.frame.FrameInstance; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.EncapsulatingNodeReference; import com.oracle.truffle.api.nodes.Node; @@ -200,11 +203,27 @@ public final void execute(PythonContext context, Access access) { resetEncapsulatingNode = true; } } + PFrame.Reference injectedCallerInfo = null; + AsyncHandler asyncHandler = context.getAsyncHandler(); + if (CallerFlags.needsFrameReference(asyncHandler.rootNode.getCallerFlags()) && context.peekTopFrameInfo(language) == null) { + Frame callerFrame = ReadFrameNode.getCurrentFrame(encapsulatingNodeRef.get(), FrameInstance.FrameAccess.READ_ONLY, + CallerFlags.NEEDS_FRAME_REFERENCE); + injectedCallerInfo = callerFrame != null ? PArguments.getCurrentFrameInfo(callerFrame) : PFrame.Reference.EMPTY; + // SimpleIndirectInvokeNode passes caller information through the thread + // state when invoked without a VirtualFrame. Async actions do not have a + // normal call site that could prepare it, so supply the interrupted frame + // after CallRootNode has requested it. + threadState.setTopFrameInfo(injectedCallerInfo); + } try { - CallDispatchers.SimpleIndirectInvokeNode.executeUncached(context.getAsyncHandler().getCallTarget(), args); + CallDispatchers.SimpleIndirectInvokeNode.executeUncached(asyncHandler.getCallTarget(), args); } catch (PException e) { handleException(e); } finally { + if (injectedCallerInfo != null) { + PFrame.Reference restoredCallerInfo = threadState.popTopFrameInfo(); + assert restoredCallerInfo == injectedCallerInfo; + } if (resetEncapsulatingNode) { encapsulatingNodeRef.set(prev); }