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/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_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.test/src/tests/test_mro.py b/graalpython/com.oracle.graal.python.test/src/tests/test_mro.py index 2364ee91fb..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 @@ -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 @@ -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.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/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 a9d31dc43c..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 @@ -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; @@ -587,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()) { @@ -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, + TruffleString key, LookupAttributeInMRONode.CachedKeyFastPath 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(inliningTarget, cachedShape.getDynamicType(), key); 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("create(key)") LookupAttributeInMRONode getDesc, + @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, key)") PropertyGetter getter, + @Cached LookupAttributeInMRONode.CachedKeyFastPath getDesc, @Cached InlineWeakValueProfile slotsValueProfile, - @Bind("loadInstanceValue(inliningTarget, receiver, getDesc, cachedShape, cachedPropertyGetter, slotsValueProfile)") Object value) { + @Bind("loadInstanceValue(inliningTarget, receiver, managedClass, key, 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,34 +2081,56 @@ 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, TruffleString key, PythonManagedClass managedClass, Shape cachedShape, LookupAttributeInMRONode.CachedKeyFastPath 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(inliningTarget, cachedShape.getDynamicType(), key); + return descr != null && (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, 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("create(key)") LookupAttributeInMRONode getDesc, + @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, + @Cached("canSkipDescriptorCheck(cachedShape, key)") boolean skipDescriptorCheck, + @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 20e2d18bd4..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 @@ -41,42 +41,64 @@ 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.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)") + @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 +112,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, 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); } } 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: