diff --git a/graalpython/com.oracle.graal.python.pegparser/src/com/oracle/graal/python/pegparser/tokenizer/SourceRange.java b/graalpython/com.oracle.graal.python.pegparser/src/com/oracle/graal/python/pegparser/tokenizer/SourceRange.java index 63e788dfd7..5bc0e0d9c1 100644 --- a/graalpython/com.oracle.graal.python.pegparser/src/com/oracle/graal/python/pegparser/tokenizer/SourceRange.java +++ b/graalpython/com.oracle.graal.python.pegparser/src/com/oracle/graal/python/pegparser/tokenizer/SourceRange.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 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 @@ -64,6 +64,14 @@ public SourceRange withEnd(int newEndLine, int newEndColumn) { return new SourceRange(startLine, startColumn, newEndLine, newEndColumn); } + public SourceRange startLineShiftColumn(int shift) { + assert shift >= 0; + if (shift == 0) { + return this; + } + return new SourceRange(startLine, startColumn, startLine, startColumn + shift); + } + public SourceRange shiftStartRight(int columns) { assert columns >= 0; if (columns == 0) { diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java index b31f3b9966..f6cca17770 100644 --- a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -59,9 +59,7 @@ import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Context.Builder; import org.graalvm.polyglot.Source; -import org.graalvm.polyglot.Value; import org.junit.After; -import org.junit.Assume; import org.junit.Before; import org.junit.Test; @@ -95,11 +93,9 @@ public void dispose() { @Test public void testSteppingAsExpected() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); // test that various elements step as expected, including generators, statement level atomic // expressions, and roots + boolean isBytecodeDLS = isBytecodeDSL(); final Source source = Source.newBuilder("python", "" + "import sys\n" + "from sys import version\n" + @@ -164,16 +160,30 @@ public void testSteppingAsExpected() throws Throwable { assertEquals(7, frame.getSourceSection().getStartLine()); event.prepareStepOver(1); }); + // for i in >genfunc()<: expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); assertEquals(8, frame.getSourceSection().getStartLine()); event.prepareStepInto(1); }); - expectSuspended((SuspendedEvent event) -> { - DebugStackFrame frame = event.getTopStackFrame(); - assertEquals(12, frame.getSourceSection().getStartLine()); - event.prepareStepOver(1); - }); + // Unlike the manual interpreter, which reports yield as onReturn, the Bytecode DSL + // correctly reports yield as onYield on the probe node, but the step over strategy + // is different for yield vs return - see SteppingStrategy$StepOver#step + if (!isBytecodeDLS) { + // steppping into genfunc() + expectSuspended((SuspendedEvent event) -> { + DebugStackFrame frame = event.getTopStackFrame(); + assertEquals(12, frame.getSourceSection().getStartLine()); + event.prepareStepOver(1); + }); + } else { + // steppping into genfunc() + expectSuspended((SuspendedEvent event) -> { + DebugStackFrame frame = event.getTopStackFrame(); + assertEquals(12, frame.getSourceSection().getStartLine()); + event.prepareStepOut(1); + }); + } expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); assertEquals(8, frame.getSourceSection().getStartLine()); @@ -190,9 +200,6 @@ public void testSteppingAsExpected() throws Throwable { @Test public void testException() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); final Source source = Source.newBuilder("python", "" + "try:\n" + " 1 / 0\n" + @@ -216,70 +223,52 @@ public void testException() throws Throwable { @Test public void testInlineEvaluation() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); final Source source = Source.newBuilder("python", "" + "y = 4\n" + "def foo(x):\n" + " a = 1\n" + " b = 2\n" + - " def bar():\n" + + " q = 42\n" + + " def bar(z = 24):\n" + " return a + b\n" + " return bar() + x + y\n" + "foo(3)", "test_inline.py").buildLiteral(); try (DebuggerSession session = tester.startSession()) { - session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(5).build()); + session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(6).build()); session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(7).build()); + session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(8).build()); tester.startEval(source); + // breakpoint at bar declaration expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); - assertEquals(5, frame.getSourceSection().getStartLine()); + assertEquals("42", frame.eval("q").toDisplayString()); assertEquals("3", frame.eval("a + b").toDisplayString()); + assertEquals(6, frame.getSourceSection().getStartLine()); event.prepareContinue(); }); + // breakpoint at bar call site expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); - assertEquals(7, frame.getSourceSection().getStartLine()); + assertEquals(8, frame.getSourceSection().getStartLine()); assertEquals("6", frame.eval("bar() * 2").toDisplayString()); event.prepareContinue(); }); - assertEquals("10", tester.expectDone()); - } - } - - @Test - @SuppressWarnings("try") - public void testBreakpointBuiltin() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); - final Source source = Source.newBuilder("python", "" + - "def foo():\n" + - " a = 1\n" + - " breakpoint()\n" + - " return 1\n" + - "foo()\n", "test_breakpoint_builtin.py").buildLiteral(); - - try (DebuggerSession session = tester.startSession()) { - tester.startEval(source); - + // breakpoint inside bar expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); - assertEquals(3, frame.getSourceSection().getStartLine()); + assertEquals("24", frame.eval("z").toDisplayString()); + assertEquals("3", frame.eval("a + b").toDisplayString()); + assertEquals(7, frame.getSourceSection().getStartLine()); event.prepareContinue(); }); - assertEquals("1", tester.expectDone()); + assertEquals("10", tester.expectDone()); } } @Test public void testConditionalBreakpointInFunction() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); final Source source = Source.newBuilder("python", "" + "def fun():\n" + " def prod(n):\n" + @@ -325,9 +314,6 @@ public void testConditionalBreakpointInFunction() throws Throwable { @Test public void testConditionalBreakpointGlobal() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); final Source source = Source.newBuilder("python", "" + "values = []\n" + "for i in range(0, 10):\n" + @@ -349,11 +335,52 @@ public void testConditionalBreakpointGlobal() throws Throwable { } } + @Test + public void testGeneralLocals() throws Throwable { + final Source source = Source.newBuilder("python", """ + def gen_fun(a, b): + yield 1 + c = 3 + yield 2 + r = 0 + for i in gen_fun(1,2): + r += i + r + """, "testGeneratorLocals.py").buildLiteral(); + + try (DebuggerSession session = tester.startSession()) { + session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(2).build()); + session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(4).build()); + tester.startEval(source); + + expectSuspended((SuspendedEvent event) -> { + DebugStackFrame frame = event.getTopStackFrame(); + assertEquals(2, frame.getSourceSection().getStartLine()); + checkStack(frame, "gen_fun", "a", "1", "b", "2"); + event.prepareContinue(); + }); + expectSuspended((SuspendedEvent event) -> { + DebugStackFrame frame = event.getTopStackFrame(); + assertEquals(2, frame.getSourceSection().getStartLine()); + event.prepareContinue(); + }); + expectSuspended((SuspendedEvent event) -> { + DebugStackFrame frame = event.getTopStackFrame(); + assertEquals(4, frame.getSourceSection().getStartLine()); + checkStack(frame, "gen_fun", "a", "1", "b", "2", "c", "3"); + event.prepareContinue(); + }); + expectSuspended((SuspendedEvent event) -> { + DebugStackFrame frame = event.getTopStackFrame(); + assertEquals(4, frame.getSourceSection().getStartLine()); + event.prepareContinue(); + }); + assertEquals("3", tester.expectDone()); + } + } + @Test public void testReenterArgumentsAndValues() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); // Test that after a re-enter, arguments are kept and variables are cleared. final Source source = Source.newBuilder("python", "" + "def main():\n" + @@ -381,7 +408,7 @@ public void testReenterArgumentsAndValues() throws Throwable { expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); - assertEquals(6, frame.getSourceSection().getStartLine()); + assertEquals("first breakpoint at 6", 6, frame.getSourceSection().getStartLine()); checkStack(frame, "fnc", "n", "11", "m", "20"); event.prepareStepOver(4); }); @@ -398,7 +425,7 @@ public void testReenterArgumentsAndValues() throws Throwable { }); expectSuspended((SuspendedEvent event) -> { DebugStackFrame frame = event.getTopStackFrame(); - assertEquals(6, frame.getSourceSection().getStartLine()); + assertEquals("first breakpoint at 6", 6, frame.getSourceSection().getStartLine()); checkStack(frame, "fnc", "n", "11", "m", "20"); }); assertEquals("50.0", tester.expectDone()); @@ -408,9 +435,6 @@ public void testReenterArgumentsAndValues() throws Throwable { @Test @SuppressWarnings("deprecation") public void testGettersSetters() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); final Source source = Source.newBuilder("python", "" + "class GetterOnly:\n" + " def __get__(self):\n" + @@ -480,9 +504,6 @@ public void testGettersSetters() throws Throwable { @Test public void testInspectJavaArray() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); final Source source = Source.newBuilder("python", "" + "import java\n" + "a_int = java.type('int[]')(3)\n" + @@ -528,11 +549,8 @@ public void testInspectJavaArray() throws Throwable { @Test public void testSourceFileURI() throws Throwable { - Value isBytecodeDLS = eval("__graalpython__.is_bytecode_dsl_interpreter"); - // GR-71618 - Assume.assumeFalse("TODO: wrong stacktrace", isBytecodeDLS.asBoolean()); if (System.getProperty("os.name").toLowerCase().contains("mac")) { - // on the mac machines we run with symlinked directories and such and it's annoying to + // on the mac machines we run with symlinked directories and such, and it's annoying to // cater for that return; } @@ -623,4 +641,8 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } }); } + + private static boolean isBytecodeDSL() { + return eval("__graalpython__.is_bytecode_dsl_interpreter").asBoolean(); + } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java index 198e40ef53..f39871cb04 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java @@ -780,7 +780,11 @@ void endTraceLineChecked(SSTNode node, Builder b) { * corresponding {@link #endSourceSection} call to ensure the Tag is closed. */ boolean beginSourceSection(SSTNode node, Builder b) { - SourceRange sourceRange = node.getSourceRange(); + return beginSourceSection(node.getSourceRange(), b); + } + + /** {@link #beginSourceSection(SSTNode, Builder)} */ + boolean beginSourceSection(SourceRange sourceRange, Builder b) { SourceRange oldSourceRange = this.currentLocation; this.currentLocation = sourceRange; @@ -4123,7 +4127,11 @@ public Void visit(StmtTy.FunctionDef node) { public void emitFunctionDef(StmtTy node, String name, ArgumentsTy args, StmtTy[] body, ExprTy[] decoratorList, ExprTy returns, TypeParamTy[] typeParams) { BytecodeLocal[] decoratorLocals = evaluateDecorators(decoratorList); - boolean newStatement = beginSourceSection(node, b); + // For instrumentation, we want to map this statement only to the declaration line, such + // that, e.g., breakpoints inside the body fire only once the body actually executes and + // not is declared. There is no simple way to get the exact line width here, so we just + // approximate it with name width. + boolean newStatement = beginSourceSection(node.getSourceRange().startLineShiftColumn(name.length()), b); emitTraceLineChecked(node, b); beginStoreLocal(name, b); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java index c537b60427..3474f52f69 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java @@ -313,6 +313,7 @@ enableSerialization = true, // enableTagInstrumentation = true, // boxingEliminationTypes = {int.class}, // + tagTreeNodeLibrary = PTagTreeNodeExports.class, // storeBytecodeIndexInFrame = true // ) @OperationProxy(PyNumberSubtractNode.class) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PTagTreeNodeExports.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PTagTreeNodeExports.java new file mode 100644 index 0000000000..e3c2845318 --- /dev/null +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PTagTreeNodeExports.java @@ -0,0 +1,63 @@ +/* + * 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.nodes.bytecode_dsl; + +import com.oracle.graal.python.runtime.interop.PythonScopes; +import com.oracle.truffle.api.bytecode.TagTreeNode; +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.interop.NodeLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; + +@ExportLibrary(value = NodeLibrary.class, receiverType = TagTreeNode.class) +final class PTagTreeNodeExports { + + @ExportMessage + static boolean hasScope(TagTreeNode node, Frame frame) { + return true; + } + + @ExportMessage + @SuppressWarnings("unused") + static Object getScope(TagTreeNode node, Frame frame, boolean nodeEnter) { + return PythonScopes.create(node, frame != null ? frame.materialize() : null); + } +} diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonLocalScope.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonLocalScope.java index d33ac746be..7a53402940 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonLocalScope.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonLocalScope.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 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 @@ -44,8 +44,14 @@ import java.util.Map; import com.oracle.graal.python.PythonLanguage; +import com.oracle.graal.python.builtins.objects.generator.PGenerator; +import com.oracle.graal.python.nodes.bytecode_dsl.BytecodeDSLCodeUnit; +import com.oracle.graal.python.nodes.bytecode_dsl.PBytecodeDSLRootNode; import com.oracle.graal.python.nodes.interop.PForeignToPTypeNode; +import com.oracle.graal.python.runtime.PythonOptions; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.bytecode.BytecodeFrame; +import com.oracle.truffle.api.bytecode.BytecodeNode; import com.oracle.truffle.api.frame.Frame; import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.MaterializedFrame; @@ -55,40 +61,52 @@ import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.library.ExportLibrary; import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.SourceSection; +import com.oracle.truffle.api.strings.TruffleString; -/** - * represents the inner local scope, which relies on the frame to retrieve variables values - */ @ExportLibrary(InteropLibrary.class) -public final class PythonLocalScope implements TruffleObject { +public abstract class PythonLocalScope implements TruffleObject { - final Map slots; - final RootNode root; - final Frame frame; - final SourceSection sourceSection; + private final Map slots; + private final RootNode root; + private final SourceSection sourceSection; - PythonLocalScope(Map slotsMap, RootNode root, Frame frame) { + PythonLocalScope(RootNode root, Map slots) { assert root != null; - this.slots = slotsMap; this.root = root; + this.slots = slots; this.sourceSection = root.getSourceSection(); - this.frame = frame; } - @TruffleBoundary - static PythonLocalScope createLocalScope(RootNode root, MaterializedFrame frame) { - LinkedHashMap slotsMap = new LinkedHashMap<>(); - - FrameDescriptor fd = frame == null ? root.getFrameDescriptor() : frame.getFrameDescriptor(); - for (int slot = 0; slot < fd.getNumberOfSlots(); slot++) { - Object identifier = fd.getSlotName(slot); - if (identifier != null && (frame == null || frame.getValue(slot) != null)) { - slotsMap.put(identifier.toString(), slot); + public static PythonLocalScope create(Node node, Frame frame) { + assert node != null; + assert frame != null; + + RootNode root = node.getRootNode(); + PBytecodeDSLRootNode dslRoot = PBytecodeDSLRootNode.cast(root); + if (PythonOptions.ENABLE_BYTECODE_DSL_INTERPRETER && dslRoot != null) { + BytecodeNode bytecodeNode = BytecodeNode.get(node); + BytecodeFrame bytecodeFrame; + if (bytecodeNode != null) { + bytecodeFrame = bytecodeNode.createMaterializedFrame(0, frame.materialize()); + } else { + assert false : String.format("root: %s, node: %s", dslRoot, node); + bytecodeFrame = null; } + return BytecodeFrameLocalScope.create(dslRoot, bytecodeFrame); + } + + Frame actualFrame = frame; + if (PGenerator.isGeneratorFrame(frame)) { + actualFrame = PGenerator.getGeneratorFrame(frame); } - return new PythonLocalScope(slotsMap, root, frame); + return FrameLocalScope.create(root, actualFrame.materialize()); + } + + public static PythonLocalScope createEmpty(RootNode root) { + return FrameLocalScope.create(root, null); } @ExportMessage @@ -108,10 +126,6 @@ Integer findFrameSlot(String member) { return slots.get(member); } - private boolean hasFrame() { - return frame != null; - } - @ExportMessage @TruffleBoundary Object readMember(String member) throws UnknownIdentifierException, UnsupportedMessageException { @@ -120,7 +134,7 @@ Object readMember(String member) throws UnknownIdentifierException, UnsupportedM if (slot == null) { throw UnknownIdentifierException.create(member); } else { - return frame.getValue(slot); + return readSlotValue(slot); } } else { throw UnsupportedMessageException.create(); @@ -146,7 +160,7 @@ boolean isMemberReadable(String member) { @ExportMessage @TruffleBoundary boolean isMemberModifiable(String member) { - return slots.containsKey(member) && frame != null; + return slots.containsKey(member) && hasFrame(); } @ExportMessage @@ -157,11 +171,10 @@ void writeMember(String member, Object value) throws UnknownIdentifierException, if (slot == null) { throw UnknownIdentifierException.create(member); } else { - frame.setObject(slot, PForeignToPTypeNode.getUncached().executeConvert(value)); + writeSlotValue(slot, PForeignToPTypeNode.getUncached().executeConvert(value)); } } else { throw UnsupportedMessageException.create(); - } } @@ -203,4 +216,96 @@ Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) { String name = root.getName(); return name == null ? "local" : name; } + + protected abstract boolean hasFrame(); + + protected abstract Object readSlotValue(int slot); + + protected abstract void writeSlotValue(int slot, Object value); + + private static final class FrameLocalScope extends PythonLocalScope { + private final MaterializedFrame frame; + + private FrameLocalScope(RootNode root, Map slots, MaterializedFrame frame) { + super(root, slots); + this.frame = frame; + } + + @TruffleBoundary + private static FrameLocalScope create(RootNode root, MaterializedFrame frame) { + LinkedHashMap slotsMap = new LinkedHashMap<>(); + FrameDescriptor fd = frame == null ? root.getFrameDescriptor() : frame.getFrameDescriptor(); + int slots = fd.getNumberOfSlots(); + for (int slot = 0; slot < slots; slot++) { + Object identifier = fd.getSlotName(slot); + if (identifier != null && (frame == null || frame.getValue(slot) != null)) { + slotsMap.put(identifier.toString(), slot); + } + } + return new FrameLocalScope(root, slotsMap, frame); + } + + @Override + protected boolean hasFrame() { + return frame != null; + } + + @Override + protected Object readSlotValue(int slot) { + return frame.getValue(slot); + } + + @Override + protected void writeSlotValue(int slot, Object value) { + frame.setObject(slot, value); + } + } + + private static final class BytecodeFrameLocalScope extends PythonLocalScope { + private final BytecodeFrame bytecodeFrame; + + private BytecodeFrameLocalScope(PBytecodeDSLRootNode root, Map slots, BytecodeFrame bytecodeFrame) { + super(root, slots); + this.bytecodeFrame = bytecodeFrame; + } + + @TruffleBoundary + private static BytecodeFrameLocalScope create(PBytecodeDSLRootNode root, BytecodeFrame bytecodeFrame) { + LinkedHashMap slotsMap = new LinkedHashMap<>(); + if (bytecodeFrame != null) { + BytecodeDSLCodeUnit codeUnit = root.getCodeUnit(); + int varIndex = 0; + varIndex = addSlots(varIndex, codeUnit.varnames, bytecodeFrame, slotsMap); + varIndex = addSlots(varIndex, codeUnit.cellvars, bytecodeFrame, slotsMap); + addSlots(varIndex, codeUnit.freevars, bytecodeFrame, slotsMap); + } + return new BytecodeFrameLocalScope(root, slotsMap, bytecodeFrame); + } + + private static int addSlots(int startIndex, TruffleString[] names, BytecodeFrame frame, LinkedHashMap slots) { + int varIndex = startIndex; + for (TruffleString name : names) { + if (frame.getLocalValue(varIndex) != null) { + slots.put(name.toJavaStringUncached(), varIndex); + } + varIndex++; + } + return varIndex; + } + + @Override + protected boolean hasFrame() { + return bytecodeFrame != null; + } + + @Override + protected Object readSlotValue(int slot) { + return bytecodeFrame.getLocalValue(slot); + } + + @Override + protected void writeSlotValue(int slot, Object value) { + bytecodeFrame.setLocalValue(slot, value); + } + } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonScopes.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonScopes.java index fd90b93f3b..97faa57973 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonScopes.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/interop/PythonScopes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 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 @@ -43,14 +43,12 @@ import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.builtins.objects.dict.PDict; import com.oracle.graal.python.builtins.objects.function.PArguments; -import com.oracle.graal.python.builtins.objects.generator.PGenerator; import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Exclusive; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.frame.Frame; -import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.UnknownIdentifierException; @@ -87,30 +85,18 @@ private static InteropMap scopeFromObject(PythonObject globals) { public static Object create(Node node, Frame frame) { RootNode root = node.getRootNode(); - PythonLocalScope localScope = PythonLocalScope.createLocalScope(root, frame != null ? frame.materialize() : null); - Object[] scopes; - if (frame != null) { - PythonObject globals = PArguments.getGlobalsSafe(frame); - MaterializedFrame generatorFrame = null; - if (PGenerator.isGeneratorFrame(frame)) { - generatorFrame = PGenerator.getGeneratorFrame(frame); - } - Object globalsScope = null; - if (globals != null) { - globalsScope = new PythonMapScope(new Object[]{scopeFromObject(globals)}, new String[]{"globals()"}); - } - if (globals != null && generatorFrame != null) { - scopes = new Object[]{localScope, globalsScope, PythonLocalScope.createLocalScope(root, generatorFrame)}; - } else if (globals != null) { - scopes = new Object[]{localScope, globalsScope}; - } else if (generatorFrame != null) { - scopes = new Object[]{localScope, PythonLocalScope.createLocalScope(root, generatorFrame)}; - } else { - return localScope; - } - } else { - return localScope; + if (frame == null) { + return PythonLocalScope.createEmpty(root); } + + Object globalsScope = null; + PythonObject globals = PArguments.getGlobalsSafe(frame); + if (globals != null) { + globalsScope = new PythonMapScope(new Object[]{scopeFromObject(globals)}, new String[]{"globals()"}); + } + + Object localScope = PythonLocalScope.create(node, frame); + Object[] scopes = globalsScope != null ? new Object[]{localScope, globalsScope} : new Object[]{localScope}; return new PythonScopes(scopes, 0); }