From 0c88fc73159b5d715abe7e8baa42b1acc417d4a0 Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 28 Apr 2026 14:33:07 +0200 Subject: [PATCH 1/4] Fast-path for builtin reads, improve get/set attribute @Operations fast-paths --- .../src/tests/test_builtin.py | 48 ++++ .../src/tests/test_mro.py | 2 +- .../src/tests/test_slot.py | 14 +- .../objects/common/DynamicObjectStorage.java | 14 +- .../objects/common/HashingStorageNodes.java | 4 +- .../builtins/objects/module/PythonModule.java | 4 +- .../oracle/graal/python/nodes/HiddenAttr.java | 11 +- .../oracle/graal/python/nodes/PGuards.java | 6 + .../bytecode_dsl/PBytecodeDSLRootNode.java | 210 ++++++++++-------- .../python/nodes/frame/ReadBuiltinNode.java | 52 ++++- .../nodes/object/GetDictIfExistsNode.java | 14 +- .../nodes/object/GetOrCreateDictNode.java | 12 +- 12 files changed, 273 insertions(+), 118 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_builtin.py b/graalpython/com.oracle.graal.python.test/src/tests/test_builtin.py index ad7bc47f6c..be03e05622 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_builtin.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_builtin.py @@ -7,6 +7,7 @@ import sys import tempfile import unittest +import io class MyIndexable(object): def __init__(self, value): @@ -121,6 +122,53 @@ def test_builtin_constants(self): self.assertEqual(getattr(builtins, 'False'), False) self.assertEqual(getattr(builtins, 'True'), True) + def test_missing_builtin_lookup_after_warmup(self): + def read_missing_builtin(): + return definitely_missing_builtin_for_review + + for _ in range(20): + with self.assertRaises(NameError): + read_missing_builtin() + + def test_instance_attr_state_machine_after_warmup(self): + class PaddedFile: + def __init__(self, fileobj, prepend=b""): + self._buffer = prepend + self._length = len(prepend) + self.file = fileobj + self._read = 0 + + def read(self, size): + if self._read is None: + return self.file.read(size) + if self._read + size <= self._length: + read = self._read + self._read += size + return self._buffer[read:self._read] + read = self._read + self._read = None + return self._buffer[read:] + self.file.read(size - self._length + read) + + def prepend(self, prepend=b""): + if self._read is None: + self._buffer = prepend + else: + self._read -= len(prepend) + return + self._length = len(self._buffer) + self._read = 0 + + def exercise(): + padded = PaddedFile(io.BytesIO(b"cdef"), b"ab") + self.assertEqual(padded.read(1), b"a") + self.assertEqual(padded.read(3), b"bcd") + padded.prepend(b"Z") + self.assertEqual(padded.read(2), b"Ze") + self.assertEqual(padded.read(2), b"f") + + for _ in range(20): + exercise() + def test_min(self): self.assertEqual(min((), default=1, key="adsf"), 1) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py b/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py index 2364ee91fb..ba1e0084f9 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, 2025, Oracle and/or its affiliates. +# Copyright (c) 2021, 2026, Oracle and/or its affiliates. # Copyright (C) 1996-2020 Python Software Foundation # # Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_slot.py b/graalpython/com.oracle.graal.python.test/src/tests/test_slot.py index 3d185a7291..37a45c5ba7 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_slot.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_slot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019, 2025, 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 @@ -173,5 +173,17 @@ class C: __slots__ = ('a', 'b') self.assertRaises(AttributeError, setattr, C(), 'c', 42) + def test_write_attr_without_dict_after_warmup(self): + class C: + __slots__ = () + + obj = C() + + def write_attr(): + obj.x = 42 + + for _ in range(20): + self.assertRaises(AttributeError, write_attr) + if __name__ == "__main__": unittest.main() diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/DynamicObjectStorage.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/DynamicObjectStorage.java index 872546b9dd..59caca476c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/DynamicObjectStorage.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/DynamicObjectStorage.java @@ -75,13 +75,18 @@ import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.object.DynamicObject; import com.oracle.truffle.api.object.Shape; -import com.oracle.truffle.api.profiles.InlinedBranchProfile; import com.oracle.truffle.api.profiles.InlinedConditionProfile; import com.oracle.truffle.api.strings.TruffleString; /** * This storage keeps a reference to the MRO when used for a type dict. Writing to this storage will * cause the appropriate attribute final assumptions to be invalidated. + *

+ * This storage may wrap a {@link PythonObject}; in that case the storage and the object can be + * used interchangeably. If the storage is transformed to other storage, or for some other reason + * requires that all accesses are routed through the {@link DynamicObjectStorage}, then the + * {@link PythonObject}'s {@link Shape} flags must be updated to include + * {@link PythonObject#HAS_MATERIALIZED_DICT}. */ public final class DynamicObjectStorage extends HashingStorage { public static final int SIZE_THRESHOLD = 100; @@ -268,6 +273,10 @@ void setStringKey(TruffleString key, Object value, DynamicObject.PutNode putNode putNode.execute(store, key, assertNoJavaString(value)); } + boolean setStringKeyIfPresent(TruffleString key, Object value, DynamicObject.PutNode putNode) { + return putNode.executeIfPresent(store, key, assertNoJavaString(value)); + } + boolean shouldTransitionOnPut() { // For now, we do not use SIZE_THRESHOLD condition to transition storages that wrap // dictionaries retrieved via object's __dict__ @@ -332,8 +341,7 @@ public static DynamicObjectStorage copy(DynamicObjectStorage receiver, public abstract static class DynamicObjectStorageSetStringKey extends SpecializedSetStringKey { @Specialization static void doIt(Node inliningTarget, HashingStorage self, TruffleString key, Object value, - @Cached DynamicObject.PutNode putNode, - @Cached InlinedBranchProfile invalidateMro) { + @Cached DynamicObject.PutNode putNode) { ((DynamicObjectStorage) self).setStringKey(key, value, putNode); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/HashingStorageNodes.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/HashingStorageNodes.java index 564d8a7b89..bdb39c0589 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/HashingStorageNodes.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/common/HashingStorageNodes.java @@ -605,11 +605,11 @@ static Object domStringKey(Frame frame, Node inliningTarget, DynamicObjectStorag if (val == PNone.NO_VALUE) { return null; } else { - putNode.execute(store, key, PNone.NO_VALUE); + self.setStringKey(key, PNone.NO_VALUE, putNode); return val; } } else { - return putNode.executeIfPresent(store, key, PNone.NO_VALUE); + return self.setStringKeyIfPresent(key, PNone.NO_VALUE, putNode); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/module/PythonModule.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/module/PythonModule.java index 617bbe8c11..962a97f1c8 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/module/PythonModule.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/module/PythonModule.java @@ -89,7 +89,7 @@ public PythonModule(Object clazz, Shape instanceShape) { setAttribute(T___SPEC__, PNone.NO_VALUE); setAttribute(T___CACHED__, PNone.NO_VALUE); setAttribute(T___FILE__, PNone.NO_VALUE); - GetOrCreateDictNode.executeUncached(this); + GetOrCreateDictNode.ensureModuleDict(this); } /** @@ -105,7 +105,7 @@ private PythonModule(PythonLanguage lang, TruffleString moduleName) { setAttribute(T___SPEC__, PNone.NONE); setAttribute(T___CACHED__, PNone.NO_VALUE); setAttribute(T___FILE__, PNone.NO_VALUE); - GetOrCreateDictNode.executeUncached(this); + GetOrCreateDictNode.ensureModuleDict(lang, this); } /** diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/HiddenAttr.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/HiddenAttr.java index 5a9a9f2c84..f5b17c320c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/HiddenAttr.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/HiddenAttr.java @@ -72,6 +72,8 @@ import com.oracle.truffle.api.nodes.UnexpectedResultException; import com.oracle.truffle.api.object.DynamicObject; import com.oracle.truffle.api.object.HiddenKey; +import com.oracle.truffle.api.object.PropertyGetter; +import com.oracle.truffle.api.object.Shape; public final class HiddenAttr { @@ -127,6 +129,10 @@ boolean hasLongValue() { this == METHOD_DEF_PTR; } + public PropertyGetter createPropertyGetter(Shape shape) { + return shape.makePropertyGetter(key); + } + @Override public String toString() { return getName(); @@ -192,10 +198,7 @@ static void doPythonObjectDict(PythonObject self, HiddenAttr attr, Object value, } private static boolean isGenericDict(PythonObject self, Object value) { - if (value instanceof PDict dict && dict.getDictStorage() instanceof DynamicObjectStorage dynamicStorage) { - return dynamicStorage.getStore() != self; - } - return true; + return !(value instanceof PDict dict && dict.getDictStorage() instanceof DynamicObjectStorage dom && dom.getStore() == self); } @Specialization(guards = "attr != DICT || !isPythonObject(self)") diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/PGuards.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/PGuards.java index 74bd1f80a9..fcbbfa477e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/PGuards.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/PGuards.java @@ -109,6 +109,7 @@ import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.object.Shape; import com.oracle.truffle.api.profiles.InlinedBranchProfile; import com.oracle.truffle.api.strings.TruffleString; import com.oracle.truffle.api.strings.TruffleString.CodeRange; @@ -127,6 +128,7 @@ public static boolean isNone(Object value) { return value == PNone.NONE; } + @Idempotent public static boolean isNoValue(Object object) { return object == PNone.NO_VALUE; } @@ -531,4 +533,8 @@ public static boolean hasBuiltinDictIter(Node inliningTarget, PDict dict, GetPyt return isBuiltinDict(dict) || getSlots.execute(inliningTarget, getClassNode.execute(inliningTarget, dict)).tp_iter() == DictBuiltins.SLOTS.tp_iter(); } + @Idempotent + public static boolean hasMaterializedDict(Shape s) { + return (s.getFlags() & PythonObject.HAS_MATERIALIZED_DICT) != 0; + } } 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 a9d31dc43c..3739b2036e 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 @@ -318,6 +318,8 @@ import com.oracle.truffle.api.source.Source; import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.api.strings.TruffleString.CodePointAtIndexUTF32Node; +import com.oracle.truffle.api.strings.TruffleString.CodePointLengthNode; import com.oracle.truffle.api.strings.TruffleStringBuilder; import com.oracle.truffle.api.strings.TruffleStringBuilderUTF32; @@ -1931,39 +1933,37 @@ public static Object doIt(VirtualFrame frame, } } - @Operation(storeBytecodeIndex = true) + @Operation(storeBytecodeIndex = false) @ConstantOperand(type = TruffleString.class) - @ImportStatic({PGuards.class, TpSlots.class, PythonUtils.class}) + @ImportStatic({PGuards.class, TpSlots.class, PythonUtils.class, DynamicObjectStorage.class}) public static final class GetAttribute { - // Builtin module object fast-path: we know there aren't any descriptors for other than - // dunder (__xxx__) names - public static Object loadModuleValue(PythonModule object, Shape cachedShape, PropertyGetter cachedPropertyGetter) { - // GetClass.GetPythonObjectClassNode would cache on the shape if it can, and read the - // dynamic type from there unless it observes objects where the type was changed. This - // is rare enough that we can pay the price of a useless read here. - Object type = cachedShape.getDynamicType(); - if (type != PythonBuiltinClassType.PythonModule) { - return null; - } + @Idempotent + public static boolean isBuiltinModule(Shape cachedShape) { + return cachedShape.getDynamicType() == PythonBuiltinClassType.PythonModule; + } - assert object.checkDictFlags(); - if ((cachedShape.getFlags() & (PythonObject.HAS_MATERIALIZED_DICT)) == 0) { - Object value = cachedPropertyGetter.get(object); - return value == PNone.NO_VALUE ? null : value; - } + @Idempotent + public static boolean canBeSpecialMethod(TruffleString key) { + return TpSlots.canBeSpecialMethod(key, CodePointLengthNode.getUncached(), CodePointAtIndexUTF32Node.getUncached()); + } - return null; + public static Object getValue(PropertyGetter getter, PythonObject obj) { + assert obj.checkDictFlags(); + return getter.get(obj); } @ForceQuickening - @Specialization(guards = {"cachedPropertyGetter != null", "cachedPropertyGetter.accepts(receiver)", "value != null", - "!canBeSpecialMethod(key, codePointLengthNode, codePointAtIndexNode)"}, limit = "3") + @Specialization(guards = { + "!canBeSpecialMethod(cachedKey)", // + "!hasMaterializedDict(cachedShape)", "isBuiltinModule(cachedShape)", // + "getter != null", "getter.accepts(receiver)", // + "!isNoValue(value)"}, limit = "3") static Object doModule(TruffleString key, PythonModule receiver, + @Cached("key") TruffleString cachedKey, @Cached("receiver.getShape()") Shape cachedShape, - @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter cachedPropertyGetter, - @Exclusive @Cached TruffleString.CodePointLengthNode codePointLengthNode, - @Exclusive @Cached TruffleString.CodePointAtIndexUTF32Node codePointAtIndexNode, - @Bind("loadModuleValue(receiver, cachedShape, cachedPropertyGetter)") Object value) { + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter getter, + @Bind("getValue(getter, receiver)") Object value) { + assert key == cachedKey; // should be a constant operand return value; } @@ -1971,91 +1971,97 @@ static Object doModule(TruffleString key, PythonModule receiver, // (__xxx__), so we can skip descriptor check + we need to check the __get__ (tp_descr_get) // on the resulting value (this is common situation) public static Object loadTypeInstanceValue(VirtualFrame frame, Node inliningTarget, PythonManagedClass object, GetObjectSlotsNode getValueSlotsNode, - CallSlotDescrGet callSlotDescrGet, Shape cachedShape, PropertyGetter cachedPropertyGetter, InlinedBranchProfile hasNonDescriptorValueProfile) { - Object type = cachedShape.getDynamicType(); - if (type != PythonBuiltinClassType.PythonClass) { - return null; - } + CallSlotDescrGet callSlotDescrGet, PropertyGetter cachedPropertyGetter, InlinedBranchProfile hasNonDescriptorValueProfile) { assert object.checkDictFlags(); - if ((cachedShape.getFlags() & (PythonObject.HAS_MATERIALIZED_DICT)) == 0) { - Object value = cachedPropertyGetter.get(object); - if (value != PNone.NO_VALUE && value != null) { - var valueGet = getValueSlotsNode.execute(inliningTarget, value).tp_descr_get(); - if (valueGet == null) { - hasNonDescriptorValueProfile.enter(inliningTarget); - return value; - } else { - return callSlotDescrGet.execute(frame, inliningTarget, valueGet, value, PNone.NO_VALUE, object); - } + Object value = cachedPropertyGetter.get(object); + if (value != PNone.NO_VALUE) { + var valueGet = getValueSlotsNode.execute(inliningTarget, value).tp_descr_get(); + if (valueGet == null) { + hasNonDescriptorValueProfile.enter(inliningTarget); + return value; + } else { + return callSlotDescrGet.execute(frame, inliningTarget, valueGet, value, PNone.NO_VALUE, object); } } return null; } + @Idempotent + public static boolean isBuiltinType(Shape cachedShape) { + return cachedShape.getDynamicType() == PythonBuiltinClassType.PythonClass; + } + @ForceQuickening - @Specialization(guards = {"cachedPropertyGetter != null", "cachedPropertyGetter.accepts(receiver)", "value != null", - "!canBeSpecialMethod(key, codePointLengthNode, codePointAtIndexNode)"}, limit = "3") + @StoreBytecodeIndex // we may be calling the descriptor + @Specialization(guards = { + "!canBeSpecialMethod(cachedKey)", // + "!hasMaterializedDict(cachedShape)", "isBuiltinType(cachedShape)", // + "getter != null", "getter.accepts(receiver)", // + "value != null"}, limit = "3") static Object doType(VirtualFrame frame, TruffleString key, PythonManagedClass receiver, + @Cached("key") TruffleString cachedKey, @Cached("receiver.getShape()") Shape cachedShape, - @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter cachedPropertyGetter, + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter getter, @Cached GetObjectSlotsNode getObjectSlotsNode, @Cached CallSlotDescrGet callSlotDescrGet, @Cached InlinedBranchProfile hasNonDescriptorValueProfile, - @Exclusive @Cached TruffleString.CodePointLengthNode codePointLengthNode, - @Exclusive @Cached TruffleString.CodePointAtIndexUTF32Node codePointAtIndexNode, - @Bind("loadTypeInstanceValue(frame, $node, receiver, getObjectSlotsNode, callSlotDescrGet, cachedShape, cachedPropertyGetter, hasNonDescriptorValueProfile)") Object value) { + @Bind("loadTypeInstanceValue(frame, $node, receiver, getObjectSlotsNode, callSlotDescrGet, getter, hasNonDescriptorValueProfile)") Object value) { + assert key == cachedKey; // should be a constant operand return value; } - // Object instance field fast-path: for cases where there is no descriptor, and it's just - // simple DOM property read - public static Object loadInstanceValue(Node inliningTarget, PythonObject object, LookupAttributeInMRONode getDesc, Shape cachedShape, PropertyGetter cachedPropertyGetter, + // The convention is that if klass is null, then the object is assumed to be of a builtin type that has the object's or module's tp_getattro - we do not need to recheck that dynamically + public static Object loadInstanceValue(Node inliningTarget, PythonObject object, PythonManagedClass klass, + LookupAttributeInMRONode getDesc, Shape cachedShape, PropertyGetter cachedPropertyGetter, InlineWeakValueProfile slotsValueProfile) { - TpSlots slots; - Object type = cachedShape.getDynamicType(); - // If this path works out, PropertyGetter.accepts() guards on the shape. - // After PE the final dynamicType field should dominate the branch and PE should remove - // the slots branch it doesn't need. The - // PythonBuiltinClassType slots are final, so PE can use that, but PythonManagedClass - // slots are not, so we should probably profile? - if (type instanceof PythonBuiltinClassType pbct) { - slots = pbct.getSlots(); - } else if (type instanceof PythonManagedClass klass) { - slots = slotsValueProfile.execute(inliningTarget, klass.getTpSlots()); - } else { - return null; - } - // The next check will fold after PE if the pbct was constant, which is implied by the - // guard in getDesc - if (slots.tp_getattro() == ObjectBuiltins.SLOTS.tp_getattro() || - slots.tp_getattro() == ModuleBuiltins.SLOTS.tp_getattro()) { - Object descr = getDesc.execute(type); + if (klass == null || hasObjectOrModuleGetattro(inliningTarget, klass, slotsValueProfile)) { + Object descr = getDesc.execute(cachedShape.getDynamicType()); if (descr == PNone.NO_VALUE) { assert object.checkDictFlags(); - if ((cachedShape.getFlags() & (PythonObject.HAS_MATERIALIZED_DICT)) == 0) { - Object value = cachedPropertyGetter.get(object); - // Note: the NO_VALUE check is harmless for PE, because it leads to a deopt - // anyway - return value == PNone.NO_VALUE ? null : value; - } + Object value = cachedPropertyGetter.get(object); + return value == PNone.NO_VALUE ? null : value; } } return null; } + private static boolean hasObjectOrModuleGetattro(Node inliningTarget, PythonManagedClass klass, InlineWeakValueProfile slotsValueProfile) { + TpSlots slots = slotsValueProfile.execute(inliningTarget, klass.getTpSlots()); + return hasObjectOrModuleGetattro(slots); + } + + private static boolean hasObjectOrModuleGetattro(TpSlots slots) { + return slots.tp_getattro() == ObjectBuiltins.SLOTS.tp_getattro() || slots.tp_getattro() == ModuleBuiltins.SLOTS.tp_getattro(); + } + + public static PythonManagedClass getManagedClassOrNull(Shape cachedShape) { + return cachedShape.getDynamicType() instanceof PythonManagedClass managedClass ? managedClass : null; + } + + @Idempotent + public static boolean isBuiltinWithObjectOrModuleGetattro(Shape cachedShape) { + return cachedShape.getDynamicType() instanceof PythonBuiltinClassType type && hasObjectOrModuleGetattro(type.getSlots()); + } + @ForceQuickening - @Specialization(guards = {"cachedPropertyGetter != null", "cachedPropertyGetter.accepts(receiver)", "value != null"}, replaces = "doModule", limit = "3") + @StoreBytecodeIndex // looking up attribute in MRO may have side effects + @Specialization(guards = { + "!hasMaterializedDict(cachedShape)", "managedClass != null || isBuiltinWithObjectOrModuleGetattro(cachedShape)", // + "getter != null", "getter.accepts(receiver)", // + "value != null"}, replaces = "doModule", limit = "3") static Object doInstanceValue(TruffleString key, PythonObject receiver, @Bind Node inliningTarget, @Cached("receiver.getShape()") Shape cachedShape, - @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter cachedPropertyGetter, + @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter getter, @Cached("create(key)") LookupAttributeInMRONode getDesc, @Cached InlineWeakValueProfile slotsValueProfile, - @Bind("loadInstanceValue(inliningTarget, receiver, getDesc, cachedShape, cachedPropertyGetter, slotsValueProfile)") Object value) { + @Bind("loadInstanceValue(inliningTarget, receiver, managedClass, getDesc, cachedShape, getter, slotsValueProfile)") Object value) { return value; } @Specialization(excludeForUncached = true, replaces = {"doInstanceValue", "doType"}) + @StoreBytecodeIndex public static Object doIt(VirtualFrame frame, TruffleString key, Object obj, @@ -2065,6 +2071,7 @@ public static Object doIt(VirtualFrame frame, @Specialization(replaces = "doIt") @InliningCutoff + @StoreBytecodeIndex public static Object doItUncached(VirtualFrame frame, TruffleString key, Object obj, @Bind Node inliningTargetForDummy, @Cached PyObjectGetAttr dummyToForceStoreBCI) { @@ -2074,33 +2081,54 @@ public static Object doItUncached(VirtualFrame frame, TruffleString key, Object @Operation(storeBytecodeIndex = true) @ConstantOperand(type = TruffleString.class) + @ImportStatic(PGuards.class) public static final class SetAttribute { - public static boolean canStoreInstanceValue(Node inliningTarget, PythonObject object, Shape cachedShape, LookupAttributeInMRONode getDesc, + @NonIdempotent + public static boolean canStoreInstanceValue(Node inliningTarget, PythonManagedClass managedClass, Shape cachedShape, LookupAttributeInMRONode getDesc, GetObjectSlotsNode getDescSlotsNode, InlineWeakValueProfile slotsValueProfile) { - TpSlots slots; - Object type = cachedShape.getDynamicType(); - if (type instanceof PythonBuiltinClassType pbct) { - slots = pbct.getSlots(); - } else if (type instanceof PythonManagedClass klass) { - slots = slotsValueProfile.execute(inliningTarget, klass.getTpSlots()); - } else { - return false; + if (managedClass == null || slotsValueProfile.execute(inliningTarget, managedClass.getTpSlots()).tp_setattro() == ObjectBuiltins.SLOTS.tp_setattro()) { + Object descr = getDesc.execute(cachedShape.getDynamicType()); + return descr == PNone.NO_VALUE || getDescSlotsNode.execute(inliningTarget, descr).tp_descr_set() == null; } - if (slots.tp_setattro() == ObjectBuiltins.SLOTS.tp_setattro()) { - Object descr = getDesc.execute(type); - if (descr == PNone.NO_VALUE || getDescSlotsNode.execute(inliningTarget, descr).tp_descr_set() == null) { - assert object.checkDictFlags(); - return (cachedShape.getFlags() & (PythonObject.HAS_MATERIALIZED_DICT | PythonObject.HAS_SLOTS_BUT_NO_DICT_FLAG)) == 0; + return false; + } + + public static boolean canSkipDescriptorCheck(Shape cachedShape, TruffleString key) { + if (cachedShape.getDynamicType() instanceof PythonBuiltinClassType type && type.getSlots().tp_setattro() == ObjectBuiltins.SLOTS.tp_setattro()) { + Object descr = LookupAttributeInMRONode.Dynamic.getUncached().execute(type, key); + if (descr == PNone.NO_VALUE) { + return true; } + return descr instanceof PythonObject pyDescr && pyDescr.getPythonClass() instanceof PythonBuiltinClassType descrType && + descrType.getSlots().tp_descr_set() == null; } return false; } + @Idempotent + public static boolean isBuiltinWithObjectSetattro(Shape cachedShape) { + return cachedShape.getDynamicType() instanceof PythonBuiltinClassType type && type.getSlots().tp_setattro() == ObjectBuiltins.SLOTS.tp_setattro(); + } + + public static PythonManagedClass getManagedClassOrNull(Shape cachedShape) { + return cachedShape.getDynamicType() instanceof PythonManagedClass managedClass ? managedClass : null; + } + + @Idempotent + public static boolean hasNoSlotsOrMaterializedDict(Shape cachedShape) { + return (cachedShape.getFlags() & (PythonObject.HAS_MATERIALIZED_DICT | PythonObject.HAS_SLOTS_BUT_NO_DICT_FLAG)) == 0; + } + @ForceQuickening - @Specialization(guards = {"cachedShape.check(receiver)", "canStoreInstanceValue(inliningTarget, receiver, cachedShape, getDesc, getDescSlotsNode, slotsValueProfile)"}, limit = "3") + @Specialization(guards = { + "hasNoSlotsOrMaterializedDict(cachedShape)", "managedClass != null || isBuiltinWithObjectSetattro(cachedShape)", // + "cachedShape.check(receiver)", // + "skipDescriptorCheck || canStoreInstanceValue(inliningTarget, managedClass, cachedShape, getDesc, getDescSlotsNode, slotsValueProfile)"}, limit = "3") static void doInstanceValue(TruffleString key, Object value, PythonObject receiver, @Bind Node inliningTarget, @Cached("receiver.getShape()") Shape cachedShape, + @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, + @Cached("canSkipDescriptorCheck(cachedShape, key)") boolean skipDescriptorCheck, @Cached("create(key)") LookupAttributeInMRONode getDesc, @Cached GetObjectSlotsNode getDescSlotsNode, @Cached InlineWeakValueProfile slotsValueProfile, diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java index 20e2d18bd4..70a5e58874 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java @@ -41,42 +41,70 @@ package com.oracle.graal.python.nodes.frame; import com.oracle.graal.python.builtins.objects.PNone; +import com.oracle.graal.python.builtins.objects.common.DynamicObjectStorage; import com.oracle.graal.python.builtins.objects.module.PythonModule; import com.oracle.graal.python.nodes.BuiltinNames; +import com.oracle.graal.python.nodes.PGuards; import com.oracle.graal.python.nodes.PNodeWithContext; import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.nodes.attributes.ReadAttributeFromModuleNode; +import com.oracle.graal.python.nodes.object.GetDictIfExistsNode; import com.oracle.graal.python.runtime.PythonContext; import com.oracle.graal.python.runtime.exception.PException; +import com.oracle.graal.python.util.PythonUtils; import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.HostCompilerDirectives.InliningCutoff; 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.dsl.GenerateInline; import com.oracle.truffle.api.dsl.GenerateUncached; +import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.NeverDefault; +import com.oracle.truffle.api.dsl.NonIdempotent; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.object.DynamicObject.GetNode; +import com.oracle.truffle.api.object.PropertyGetter; +import com.oracle.truffle.api.object.Shape; import com.oracle.truffle.api.profiles.InlinedConditionProfile; import com.oracle.truffle.api.strings.TruffleString; @GenerateUncached @GenerateInline(false) // footprint reduction 40 -> 21 +@ImportStatic({GetDictIfExistsNode.class, DynamicObjectStorage.class, PGuards.class, PythonUtils.class}) public abstract class ReadBuiltinNode extends PNodeWithContext { public abstract Object execute(TruffleString attributeId); - // TODO: (tfel) Think about how we can globally catch writes to the builtin - // module so we can treat anything read from it as constant here. - @Specialization(guards = "isSingleContext(this)") + @NeverDefault + static Object readAttribute(DynamicObjectStorage domStorage, TruffleString name) { + return GetNode.getUncached().execute(domStorage.getStore(), name, PNone.NO_VALUE); + } + + @NonIdempotent + static boolean getterAccepts(PropertyGetter getter, PythonModule mod) { + return getter.accepts(mod); + } + + @NonIdempotent + static Object getterGet(PropertyGetter getter, PythonModule mod) { + assert mod.checkDictFlags(); + return getter.get(mod); + } + + // Fast-path: caches builtins PythonModule object (single context), checks that it's __dict__ + // hasn't changed shape and cached property getter to reads the value directly + @Specialization(excludeForUncached = true, guards = {"isSingleContext(this)", + "!hasMaterializedDict(cachedShape)", // + "getter != null", // + "getterAccepts(getter, builtins)", // shape check including the HAS_MATERIALIZED_DICT flag + "!isNoValue(value)"}) Object returnBuiltinFromConstantModule(TruffleString attributeId, - @Bind Node inliningTarget, - @Exclusive @Cached PRaiseNode raiseNode, - @Exclusive @Cached InlinedConditionProfile isBuiltinProfile, - @Shared @Cached ReadAttributeFromModuleNode readFromBuiltinsNode, - @Cached(value = "getBuiltins()", allowUncached = true) PythonModule builtins) { - return readBuiltinFromModule(attributeId, raiseNode, inliningTarget, isBuiltinProfile, builtins, readFromBuiltinsNode); + @Cached("getBuiltins()") PythonModule builtins, + @Cached("builtins.getShape()") Shape cachedShape, + @Cached(value = "getPropertyGetterWithFinalAssumption(cachedShape, attributeId)", neverDefault = false) PropertyGetter getter, + @Bind("getterGet(getter, builtins)") Object value) { + return value; } @InliningCutoff @@ -90,10 +118,10 @@ Object returnBuiltin(TruffleString attributeId, @Bind Node inliningTarget, @Exclusive @Cached PRaiseNode raiseNode, @Exclusive @Cached InlinedConditionProfile isBuiltinProfile, - @Shared @Cached ReadAttributeFromModuleNode readFromBuiltinsNode, + @Cached ReadAttributeFromModuleNode readFromBuiltinsNode, @Exclusive @Cached InlinedConditionProfile ctxInitializedProfile) { PythonModule builtins = getBuiltins(inliningTarget, ctxInitializedProfile); - return returnBuiltinFromConstantModule(attributeId, inliningTarget, raiseNode, isBuiltinProfile, readFromBuiltinsNode, builtins); + return readBuiltinFromModule(attributeId, raiseNode, inliningTarget, isBuiltinProfile, builtins, readFromBuiltinsNode); } private static Object readBuiltinFromModule(TruffleString attributeId, PRaiseNode raiseNode, Node inliningTarget, diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetDictIfExistsNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetDictIfExistsNode.java index d3d0bfaf34..56d92d68ad 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetDictIfExistsNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetDictIfExistsNode.java @@ -61,13 +61,13 @@ import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.builtins.objects.type.PythonManagedClass; import com.oracle.graal.python.builtins.objects.type.TypeNodes.IsTypeNode; -import com.oracle.graal.python.runtime.nativeaccess.NativeFunctionPointer; import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.HiddenAttr; import com.oracle.graal.python.nodes.HiddenAttr.ReadNode; import com.oracle.graal.python.nodes.PNodeWithContext; import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.runtime.IndirectCallData.BoundaryCallData; +import com.oracle.graal.python.runtime.nativeaccess.NativeFunctionPointer; import com.oracle.graal.python.runtime.object.PFactory; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.HostCompilerDirectives.InliningCutoff; @@ -80,6 +80,8 @@ import com.oracle.truffle.api.dsl.NeverDefault; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.PropertyGetter; import com.oracle.truffle.api.object.Shape; import com.oracle.truffle.api.profiles.InlinedBranchProfile; @@ -93,8 +95,18 @@ public static GetDictIfExistsNode create() { public abstract PDict execute(Object object); + public abstract PDict execute(PythonAbstractNativeObject object); + public abstract PDict execute(PythonObject object); + /** + * Use this node when the shape is already cached. Use + * {@link PropertyGetter#accepts(DynamicObject)} to check the cached shape in a guard. Note that this does not initialize the final property assumption! + */ + public static PropertyGetter createDictPropertyGetter(Shape shape) { + return HiddenAttr.DICT.createPropertyGetter(shape); + } + @Specialization(guards = {"object.getShape() == cachedShape", "hasNoDict(cachedShape)"}, limit = "1") static PDict getNoDictCachedShape(@SuppressWarnings("unused") PythonObject object, @SuppressWarnings("unused") @Cached("object.getShape()") Shape cachedShape) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetOrCreateDictNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetOrCreateDictNode.java index cdf3323ca0..e8a8abeb98 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetOrCreateDictNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetOrCreateDictNode.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,6 +44,7 @@ import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.builtins.objects.dict.PDict; +import com.oracle.graal.python.builtins.objects.module.PythonModule; import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.PGuards; @@ -78,6 +79,15 @@ public static PDict executeUncached(Object object) { return GetOrCreateDictNodeGen.getUncached().execute(null, object); } + public static void ensureModuleDict(PythonModule module) { + ensureModuleDict(PythonLanguage.get(null), module); + } + + public static void ensureModuleDict(PythonLanguage language, PythonModule module) { + var dict = PFactory.createDictFixedStorage(language, module); + SetDictNode.executeUncached(module, dict); + } + @Specialization static PDict doPythonObject(Node inliningTarget, PythonObject object, @Shared("getDict") @Cached(inline = false) GetDictIfExistsNode getDictIfExistsNode, From fd2f6c0291f29102d86e3b8e992cf359cd36fc87 Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 16 Jun 2026 17:43:14 +0200 Subject: [PATCH 2/4] Add unittest run with only uncached interpreter and only cached interpreter --- ci.jsonnet | 12 ++++++++++++ mx.graalpython/mx_graalpython.py | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/ci.jsonnet b/ci.jsonnet index ed06e0e4f2..dab3791c05 100644 --- a/ci.jsonnet +++ b/ci.jsonnet @@ -137,6 +137,12 @@ local with_compiler = task_spec({ dynamic_imports +:: ["/compiler"], }), + local unittest_args_gate(args) = task_spec({ + tags:: "python-unittest", + environment +: { + GRAALPY_UNITTEST_ARGS: std.join(" ", args), + }, + }), // ----------------------------------------------------------------------------------------------------------------- // @@ -166,6 +172,12 @@ "python-unittest-native-debug-build": gpgate + platform_spec(no_jobs) + native_debug_build_gate("python-unittest") + platform_spec({ "linux:amd64:jdk-latest" : tier3, }), + "python-unittest-cached-interpreter": gpgate + unittest_args_gate(["--python.UncachedInterpreterThreshold=0"]) + platform_spec(no_jobs) + platform_spec({ + "linux:amd64:jdk-latest" : tier3 + require(GPY_JVM_STANDALONE), + }), + "python-unittest-uncached-interpreter": gpgate + unittest_args_gate(["--python.ForceUncachedInterpreter=true"]) + platform_spec(no_jobs) + platform_spec({ + "linux:amd64:jdk-latest" : tier3 + require(GPY_JVM_STANDALONE), + }), "python-unittest-multi-context": gpgate + require(GPY_NATIVE_STANDALONE) + platform_spec(no_jobs) + platform_spec({ "linux:amd64:jdk-latest" : tier3, "linux:aarch64:jdk-latest" : daily + t("02:00:00"), diff --git a/mx.graalpython/mx_graalpython.py b/mx.graalpython/mx_graalpython.py index 68c3b6c0d3..1f5dd4463f 100644 --- a/mx.graalpython/mx_graalpython.py +++ b/mx.graalpython/mx_graalpython.py @@ -1589,11 +1589,15 @@ def run_python_unittests(python_binary, args=None, paths=None, exclude=None, env parallelism = str(min(os.cpu_count() or 1, parallel)) args = args or [] + extra_args = shlex.split(os.environ.get("GRAALPY_UNITTEST_ARGS", "")) + if extra_args: + mx.log("Adding GraalPy unittest args from GRAALPY_UNITTEST_ARGS: " + shlex.join(extra_args)) args = [ "--vm.ea", "--experimental-options=true", "--python.EnableDebuggingBuiltins", *args, + *extra_args, ] if env is None: From 73a9cec7b2e5592e580a4fef4367486c8aa1d330 Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 16 Jun 2026 17:26:31 +0200 Subject: [PATCH 3/4] Add fast path variant for LookupAttributeInMRONode --- .../src/tests/test_mro.py | 14 +++- .../attributes/LookupAttributeInMRONode.java | 82 ++++++++++++++++++- .../bytecode_dsl/PBytecodeDSLRootNode.java | 19 +++-- .../python/nodes/frame/ReadBuiltinNode.java | 6 -- 4 files changed, 100 insertions(+), 21 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py b/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py index ba1e0084f9..3b4b7da6f1 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py @@ -182,12 +182,20 @@ def __eq__(self, other): eq_called.append(1) X.__bases__ = (Base2,) + class Descr: + def __init__(self, value): + self.value = value + def __get__(self, obj, owner=None): + return self.value + def __set__(self, obj, value): + pass + class Base(object): - mykey = 'base 42' + mykey = Descr('base 42') def __str__(self): return 'Base' class Base2(object): - mykey = 'base2 42' + mykey = Descr('base2 42') def __str__(self): return 'Base2' X = type('X', (Base,), {MyKey(): 5}) @@ -202,6 +210,8 @@ def __str__(self): return 'Base2' eq_called = [] X = type('X', (Base,), {MyKey(): 5}) xobj = X() + xobj_dict = object.__getattribute__(xobj, "__dict__") + xobj_dict['mykey'] = 'false lead' assert str(xobj) == 'Base' assert xobj.mykey == 'base 42' assert eq_called == [1] diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/LookupAttributeInMRONode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/LookupAttributeInMRONode.java index 93885bcf16..d43fe87a89 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/LookupAttributeInMRONode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/LookupAttributeInMRONode.java @@ -44,12 +44,14 @@ import com.oracle.graal.python.builtins.Python3Core; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.objects.PNone; +import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.builtins.objects.type.MroShape; import com.oracle.graal.python.builtins.objects.type.MroShape.MroShapeLookupResult; import com.oracle.graal.python.builtins.objects.type.PythonAbstractClass; import com.oracle.graal.python.builtins.objects.type.PythonClass; import com.oracle.graal.python.builtins.objects.type.TypeNodes.GetMroStorageNode; import com.oracle.graal.python.builtins.objects.type.TypeNodes.IsSameTypeNode; +import com.oracle.graal.python.nodes.PGuards; import com.oracle.graal.python.nodes.PNodeWithContext; import com.oracle.graal.python.runtime.PythonContext; import com.oracle.graal.python.runtime.PythonOptions; @@ -64,6 +66,7 @@ import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; +import com.oracle.truffle.api.dsl.GenerateCached; import com.oracle.truffle.api.dsl.GenerateInline; import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.Idempotent; @@ -75,6 +78,7 @@ import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.ExplodeLoop.LoopExplosionKind; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.object.DynamicObject; import com.oracle.truffle.api.profiles.InlinedConditionProfile; import com.oracle.truffle.api.strings.TruffleString; @@ -156,6 +160,7 @@ public static LookupAttributeInMRONode createForLookupOfUnmanagedClasses(Truffle return LookupAttributeInMRONodeGen.create(key, true); } + @NeverDefault static Object findAttr(Python3Core core, PythonBuiltinClassType klass, TruffleString key) { return findAttr(core, klass, key, ReadAttributeFromPythonObjectNode.getUncached()); } @@ -202,7 +207,22 @@ public MROChangedException(Object result) { } } + @SuppressWarnings("serial") + public static class MROGenericDictException extends StacktracelessCheckedException { + private static final MROGenericDictException INSTANCE = new MROGenericDictException(); + } + MroSequenceStorage.FinalAttributeAssumptionPair findAttrAndAssumptionInMRO(Object klass) throws MROChangedException { + try { + return findAttrAndAssumptionInMRO(this, klass, key, skipNonStaticBases, false); + } catch (MROGenericDictException ignore) { + throw CompilerDirectives.shouldNotReachHere(); + } + } + + @NeverDefault + static MroSequenceStorage.FinalAttributeAssumptionPair findAttrAndAssumptionInMRO(Node n, Object klass, TruffleString key, boolean skipNonStaticBases, + boolean mustNotSideEffect) throws MROChangedException, MROGenericDictException { CompilerAsserts.neverPartOfCompilation(); // Regarding potential side effects to MRO caused by __eq__ of the keys in the dicts that we // search through: CPython seems to read the MRO once and then compute the result also @@ -214,11 +234,13 @@ MroSequenceStorage.FinalAttributeAssumptionPair findAttrAndAssumptionInMRO(Objec if (assumptionNode != null) { return assumptionNode; } - // Put a new assumption in place in case the MRO changes during the lookup MroSequenceStorage.FinalAttributeAssumptionPair assumptionPair = new MroSequenceStorage.FinalAttributeAssumptionPair(); - mro.putFinalAttributeAssumption(key, assumptionPair); + if (!mustNotSideEffect) { + // Put a new assumption in place in case the MRO changes during the lookup + mro.putFinalAttributeAssumption(key, assumptionPair); + } EncapsulatingNodeReference nodeRef = EncapsulatingNodeReference.getCurrent(); - Node prev = nodeRef.set(this); + Node prev = nodeRef.set(n); Object result = PNone.NO_VALUE; try { for (int i = 0; i < mro.length(); i++) { @@ -226,7 +248,16 @@ MroSequenceStorage.FinalAttributeAssumptionPair findAttrAndAssumptionInMRO(Objec if (skipNonStaticBase(clsObj, skipNonStaticBases)) { continue; } - Object value = ReadAttributeFromObjectNode.getUncached().execute(clsObj, key); + Object value; + if (mustNotSideEffect) { + if (clsObj instanceof PythonObject pyClsObj && !PGuards.hasMaterializedDict(pyClsObj.getShape())) { + value = DynamicObject.GetNode.getUncached().execute(pyClsObj, key, PNone.NO_VALUE); + } else { + throw MROGenericDictException.INSTANCE; + } + } else { + value = ReadAttributeFromObjectNode.getUncached().execute(clsObj, key); + } if (value != PNone.NO_VALUE) { result = value; break; @@ -240,6 +271,10 @@ MroSequenceStorage.FinalAttributeAssumptionPair findAttrAndAssumptionInMRO(Objec // exception. This should abort the specialization throw new MROChangedException(result); } + if (mustNotSideEffect) { + // must connect the assumption with the MRO here, otherwise we may end up with half initialized FinalAttributeAssumptionPair if we had to bail out because we found a generic dict + mro.putFinalAttributeAssumption(key, assumptionPair); + } assumptionPair.setValue(result); return assumptionPair; } @@ -264,6 +299,45 @@ Object lookupSlowPath(Object klass, return slowPathNode.execute(klass, key, skipNonStaticBases); } + @GenerateInline + @GenerateCached(false) + @ImportStatic(LookupAttributeInMRONode.class) + public abstract static class CachedKeyFastPath extends PNodeWithContext { + + public final Object execute(Node inliningTarget, Object klass, TruffleString key) { + CompilerAsserts.partialEvaluationConstant(klass); + CompilerAsserts.partialEvaluationConstant(key); + try { + return executeImpl(inliningTarget, klass, key); + } catch (MROChangedException e) { + throw CompilerDirectives.shouldNotReachHere(); + } catch (MROGenericDictException e) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + return null; + } + } + + public abstract Object executeImpl(Node inliningTarget, Object klass, TruffleString key) throws MROChangedException, MROGenericDictException; + + @Specialization + static Object lookupPBCTCached(Node inliningTarget, PythonBuiltinClassType klass, TruffleString key, + @Bind PythonContext context, + @Cached("findAttr(context, klass, key)") Object cachedValue) { + return cachedValue; + } + + @Specialization(assumptions = "cachedAttrInMROInfo.getAssumption()", guards = "isSingleContext()") + static Object lookupConstantMROCached(Node inliningTarget, Object klass, TruffleString key, + @Cached("findAttrAndAssumptionInMRO(inliningTarget, klass, key, false, true)") MroSequenceStorage.FinalAttributeAssumptionPair cachedAttrInMROInfo) { + return cachedAttrInMROInfo.getValue(); + } + + @Specialization(replaces = {"lookupPBCTCached", "lookupConstantMROCached"}) + static Object noFastPath(Object klass, TruffleString key) { + return null; + } + } + public abstract static class SlowPath extends PNodeWithContext { @Child private GetMroStorageNode getMroNode; 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 3739b2036e..e3b02ffae7 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 @@ -2012,10 +2012,10 @@ static Object doType(VirtualFrame frame, TruffleString key, PythonManagedClass r // The convention is that if klass is null, then the object is assumed to be of a builtin type that has the object's or module's tp_getattro - we do not need to recheck that dynamically public static Object loadInstanceValue(Node inliningTarget, PythonObject object, PythonManagedClass klass, - LookupAttributeInMRONode getDesc, Shape cachedShape, PropertyGetter cachedPropertyGetter, + TruffleString key, LookupAttributeInMRONode.CachedKeyFastPath getDesc, Shape cachedShape, PropertyGetter cachedPropertyGetter, InlineWeakValueProfile slotsValueProfile) { if (klass == null || hasObjectOrModuleGetattro(inliningTarget, klass, slotsValueProfile)) { - Object descr = getDesc.execute(cachedShape.getDynamicType()); + Object descr = getDesc.execute(inliningTarget, cachedShape.getDynamicType(), key); if (descr == PNone.NO_VALUE) { assert object.checkDictFlags(); Object value = cachedPropertyGetter.get(object); @@ -2054,9 +2054,9 @@ static Object doInstanceValue(TruffleString key, PythonObject receiver, @Cached("receiver.getShape()") Shape cachedShape, @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter getter, - @Cached("create(key)") LookupAttributeInMRONode getDesc, + @Cached LookupAttributeInMRONode.CachedKeyFastPath getDesc, @Cached InlineWeakValueProfile slotsValueProfile, - @Bind("loadInstanceValue(inliningTarget, receiver, managedClass, getDesc, cachedShape, getter, slotsValueProfile)") Object value) { + @Bind("loadInstanceValue(inliningTarget, receiver, managedClass, key, getDesc, cachedShape, getter, slotsValueProfile)") Object value) { return value; } @@ -2084,11 +2084,11 @@ public static Object doItUncached(VirtualFrame frame, TruffleString key, Object @ImportStatic(PGuards.class) public static final class SetAttribute { @NonIdempotent - public static boolean canStoreInstanceValue(Node inliningTarget, PythonManagedClass managedClass, Shape cachedShape, LookupAttributeInMRONode getDesc, + public static boolean canStoreInstanceValue(Node inliningTarget, TruffleString key, PythonManagedClass managedClass, Shape cachedShape, LookupAttributeInMRONode.CachedKeyFastPath getDesc, GetObjectSlotsNode getDescSlotsNode, InlineWeakValueProfile slotsValueProfile) { if (managedClass == null || slotsValueProfile.execute(inliningTarget, managedClass.getTpSlots()).tp_setattro() == ObjectBuiltins.SLOTS.tp_setattro()) { - Object descr = getDesc.execute(cachedShape.getDynamicType()); - return descr == PNone.NO_VALUE || getDescSlotsNode.execute(inliningTarget, descr).tp_descr_set() == null; + Object descr = getDesc.execute(inliningTarget, cachedShape.getDynamicType(), key); + return descr != null && (descr == PNone.NO_VALUE || getDescSlotsNode.execute(inliningTarget, descr).tp_descr_set() == null); } return false; } @@ -2123,13 +2123,14 @@ public static boolean hasNoSlotsOrMaterializedDict(Shape cachedShape) { @Specialization(guards = { "hasNoSlotsOrMaterializedDict(cachedShape)", "managedClass != null || isBuiltinWithObjectSetattro(cachedShape)", // "cachedShape.check(receiver)", // - "skipDescriptorCheck || canStoreInstanceValue(inliningTarget, managedClass, cachedShape, getDesc, getDescSlotsNode, slotsValueProfile)"}, limit = "3") + "skipDescriptorCheck || canStoreInstanceValue(inliningTarget, cachedKey, managedClass, cachedShape, getDesc, getDescSlotsNode, slotsValueProfile)"}, limit = "3") static void doInstanceValue(TruffleString key, Object value, PythonObject receiver, @Bind Node inliningTarget, + @Cached("key") TruffleString cachedKey, @Cached("receiver.getShape()") Shape cachedShape, @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, @Cached("canSkipDescriptorCheck(cachedShape, key)") boolean skipDescriptorCheck, - @Cached("create(key)") LookupAttributeInMRONode getDesc, + @Cached LookupAttributeInMRONode.CachedKeyFastPath getDesc, @Cached GetObjectSlotsNode getDescSlotsNode, @Cached InlineWeakValueProfile slotsValueProfile, @Cached DynamicObject.PutNode putNode) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java index 70a5e58874..517fa98d45 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/frame/ReadBuiltinNode.java @@ -64,7 +64,6 @@ import com.oracle.truffle.api.dsl.NonIdempotent; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.Node; -import com.oracle.truffle.api.object.DynamicObject.GetNode; import com.oracle.truffle.api.object.PropertyGetter; import com.oracle.truffle.api.object.Shape; import com.oracle.truffle.api.profiles.InlinedConditionProfile; @@ -76,11 +75,6 @@ public abstract class ReadBuiltinNode extends PNodeWithContext { public abstract Object execute(TruffleString attributeId); - @NeverDefault - static Object readAttribute(DynamicObjectStorage domStorage, TruffleString name) { - return GetNode.getUncached().execute(domStorage.getStore(), name, PNone.NO_VALUE); - } - @NonIdempotent static boolean getterAccepts(PropertyGetter getter, PythonModule mod) { return getter.accepts(mod); From 88a5489a98c849850907a9ae440c12f00c997a1b Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Thu, 25 Jun 2026 11:21:26 +0200 Subject: [PATCH 4/4] Mark frames as escaped even when the first escape happens after the transition to cached. Traceback collection counted frames during exception unwinding, but did not mark them as escaped. In uncached execution, we had the PFrame.Reference links unconditionally, so this worked when it happened then, and we marked the root as needing the PFrame.Reference link, so on later cached calls we have them. But if we raise and escape the frame for the very first time when already cached we did not preserve the f_back chain and could not reconstruct it properly later. This change makes the frames that belong to a traceback mark their current PFrame.Reference as escaped while the exception is unwinding and the VirtualFrame is thus still on the stack. The CalleeContext#exit logic then materializes the frame and propagates to caller frames and this preserves the f_back chain. This can be triggered by forcing immediately cached execution, but a regression test covers the case where functions first transition to cached execution without raising, then later raises after some time (presumably after we transitioned to cached) and inspect traceback backrefs. --- .../src/tests/test_frame_tests.py | 18 ++++++++++++++++++ .../bytecode_dsl/PBytecodeDSLRootNode.java | 2 +- .../python/runtime/exception/PException.java | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_frame_tests.py b/graalpython/com.oracle.graal.python.test/src/tests/test_frame_tests.py index 64381542f8..8551270f30 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_frame_tests.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_frame_tests.py @@ -238,6 +238,24 @@ def foo(): assert e.__traceback__.tb_next.tb_frame.f_back.f_code == test_backref_from_traceback.__code__ +def test_backref_from_traceback_after_cached_transition(): + def bar(should_raise): + if should_raise: + raise RuntimeError + + def foo(should_raise): + bar(should_raise) + + for _ in range(64): # we probably do not execute 64-times in uncached + foo(False) + + try: + foo(True) + except Exception as e: + assert e.__traceback__.tb_next.tb_next.tb_frame.f_back.f_code == foo.__code__ + assert e.__traceback__.tb_next.tb_frame.f_back.f_code == test_backref_from_traceback_after_cached_transition.__code__ + + def test_frame_from_another_thread(): import sys, threading event1 = threading.Event() 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 e3b02ffae7..528a599194 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 @@ -589,7 +589,7 @@ public static void doExit(VirtualFrame frame, AbstractTruffleException ate, @Bind PBytecodeDSLRootNode root, @Bind BytecodeNode location) { if (ate instanceof PException pe) { - pe.notifyAddedTracebackFrame(!root.isInternal()); + pe.notifyAddedTracebackFrame(frame, !root.isInternal()); } // We cannot use instrumentation for exceptional exit if (root.needsTraceAndProfileInstrumentation()) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/exception/PException.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/exception/PException.java index ce299c781c..ebdc51fd95 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/exception/PException.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/exception/PException.java @@ -363,6 +363,18 @@ private void markFrameEscaped() { } } + private void markFrameEscaped(Frame frame) { + if (frame != null) { + PFrame.Reference currentFrameInfo = PArguments.getCurrentFrameInfo(frame); + if (currentFrameInfo != null) { + currentFrameInfo.markAsEscaped(); + if (escapedFrameThread == null) { + escapedFrameThread = Thread.currentThread(); + } + } + } + } + public Thread getEscapedFrameThread() { return escapedFrameThread; } @@ -401,9 +413,10 @@ public int getTracebackFrameCount() { return tracebackFrameCount; } - public void notifyAddedTracebackFrame(boolean visible) { + public void notifyAddedTracebackFrame(Frame frame, boolean visible) { if (visible) { tracebackFrameCount++; + markFrameEscaped(frame); } }