From 5c140e0d5b0fd0a971eb27b419bb7898cf034ba9 Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 15 Sep 2026 14:44:14 +0200 Subject: [PATCH 1/5] Write back the internal dict instance in type.__dict__ --- .../builtins/objects/type/TypeBuiltins.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeBuiltins.java index 7013d65b00..a6cf28b4b5 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeBuiltins.java @@ -153,7 +153,7 @@ import com.oracle.graal.python.nodes.function.builtins.PythonVarargsBuiltinNode; import com.oracle.graal.python.nodes.object.BuiltinClassProfiles.IsBuiltinClassExactProfile; import com.oracle.graal.python.nodes.object.GetClassNode; -import com.oracle.graal.python.nodes.object.GetDictIfExistsNode; +import com.oracle.graal.python.nodes.object.GetOrCreateDictNode; import com.oracle.graal.python.nodes.util.CannotCastException; import com.oracle.graal.python.nodes.util.CastToTruffleStringNode; import com.oracle.graal.python.runtime.ExecutionContext.BoundaryCallContext; @@ -798,20 +798,18 @@ static Object base(Object self, abstract static class DictNode extends PythonUnaryBuiltinNode { @Specialization Object doType(PythonBuiltinClassType self, + @Bind Node inliningTarget, @Bind PythonLanguage language, - @Shared @Cached GetDictIfExistsNode getDict) { - return doManaged(getContext().lookupType(self), language, getDict); + @Shared @Cached GetOrCreateDictNode getDict) { + return doManaged(getContext().lookupType(self), inliningTarget, language, getDict); } @Specialization static Object doManaged(PythonManagedClass self, + @Bind Node inliningTarget, @Bind PythonLanguage language, - @Shared @Cached GetDictIfExistsNode getDict) { - PDict dict = getDict.execute(self); - if (dict == null) { - dict = PFactory.createDictFixedStorage(language, self); - // The mapping is unmodifiable, so we don't have to assign it back - } + @Shared @Cached GetOrCreateDictNode getDict) { + PDict dict = getDict.execute(inliningTarget, self); return PFactory.createMappingproxy(language, dict); } From 0f36bae96e9df531141d82a44525d00399b446db Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 15 Sep 2026 16:47:10 +0200 Subject: [PATCH 2/5] Cache length in DynamicObjectStorage --- .../test/builtin/objects/dict/PDictTest.java | 48 ++++++++ .../src/tests/test_dict.py | 110 ++++++++++++++++++ .../graal/python/builtins/Python3Core.java | 4 +- .../builtins/modules/ImpModuleBuiltins.java | 6 +- .../modules/cext/PythonCextBuiltins.java | 28 ----- .../objects/common/DynamicObjectStorage.java | 74 ++++++++++-- .../objects/common/HashingStorageNodes.java | 40 ++++--- .../namespace/SimpleNamespaceBuiltins.java | 6 +- .../WriteAttributeToPythonObjectNode.java | 9 +- 9 files changed, 261 insertions(+), 64 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/objects/dict/PDictTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/objects/dict/PDictTest.java index be6ce8d515..0705893a2c 100644 --- a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/objects/dict/PDictTest.java +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/objects/dict/PDictTest.java @@ -49,10 +49,16 @@ import org.junit.Test; import com.oracle.graal.python.PythonLanguage; +import com.oracle.graal.python.builtins.objects.PNone; +import com.oracle.graal.python.builtins.objects.common.DynamicObjectStorage; import com.oracle.graal.python.builtins.objects.common.EconomicMapStorage; import com.oracle.graal.python.builtins.objects.common.HashingStorageNodes.HashingStorageDelItem; import com.oracle.graal.python.builtins.objects.common.HashingStorageNodes.HashingStorageLen; import com.oracle.graal.python.builtins.objects.dict.PDict; +import com.oracle.graal.python.builtins.objects.object.PythonObject; +import com.oracle.graal.python.nodes.attributes.WriteAttributeToObjectNode; +import com.oracle.graal.python.nodes.attributes.WriteAttributeToPythonObjectNode; +import com.oracle.graal.python.nodes.object.GetOrCreateDictNode; import com.oracle.graal.python.runtime.object.PFactory; import com.oracle.graal.python.test.PythonTests; import com.oracle.truffle.api.strings.TruffleString; @@ -81,6 +87,48 @@ static int length(PDict dict) { return HashingStorageLen.executeUncached(dict.getDictStorage()); } + @Test + public void dynamicStorageAttributeWrites() { + PythonObject object = PFactory.createSimpleNamespace(PythonLanguage.get(null)); + WriteAttributeToPythonObjectNode.executeUncached(object, ts("key"), 1); + PDict dict = GetOrCreateDictNode.executeUncached(object); + assertEquals(1, length(dict)); + WriteAttributeToObjectNode.getUncached().execute(object, ts("key"), PNone.NO_VALUE); + assertEquals(0, length(dict)); + WriteAttributeToObjectNode.getUncached().execute(object, ts("key"), 2); + assertEquals(1, length(dict)); + WriteAttributeToObjectNode.getUncached().execute(object, ts("key"), PNone.NO_VALUE); + assertEquals(0, length(dict)); + } + + @Test(expected = AssertionError.class) + public void directAttributeWriteRejectsBackingDict() { + PythonObject object = PFactory.createSimpleNamespace(PythonLanguage.get(null)); + GetOrCreateDictNode.executeUncached(object); + WriteAttributeToPythonObjectNode.executeUncached(object, ts("key"), 1); + } + + @Test(expected = AssertionError.class) + public void dynamicStorageDetectsStaleTemporaryWrapper() { + PythonObject object = PFactory.createSimpleNamespace(PythonLanguage.get(null)); + DynamicObjectStorage storage = new DynamicObjectStorage(object); + assertEquals(0, HashingStorageLen.executeUncached(storage)); + WriteAttributeToObjectNode.getUncached().execute(object, ts("key"), 1); + HashingStorageLen.executeUncached(storage); + } + + @Test(expected = AssertionError.class) + public void dynamicStorageDetectsStaleTemporaryWrapperWithDict() { + PythonObject object = PFactory.createSimpleNamespace(PythonLanguage.get(null)); + PDict dict = GetOrCreateDictNode.executeUncached(object); + DynamicObjectStorage storage = new DynamicObjectStorage(object); + assertEquals(0, HashingStorageLen.executeUncached(storage)); + assertEquals(0, length(dict)); + WriteAttributeToObjectNode.getUncached().execute(object, ts("key"), 1); + assertEquals(1, length(dict)); + HashingStorageLen.executeUncached(storage); + } + @Test public void economicMapStorageTransition() { PDict dict = PFactory.createDict(PythonLanguage.get(null)); diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py b/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py index 284cfb7297..0c35a6bcd7 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py @@ -42,6 +42,116 @@ graalpy_only = unittest.skipUnless(sys.implementation.name == "graalpy", "GraalPy-specific dict storage test") + +def test_namespace_reinitialization_with_backing_dict(): + from types import SimpleNamespace + + obj = SimpleNamespace(a=1) + mapping = obj.__dict__ + assert len(mapping) == 1 + obj.__init__(b=2) + assert len(mapping) == 2 + assert mapping == {"a": 1, "b": 2} + mapping.clear() + mapping = obj.__dict__ + assert len(mapping) == 0 + obj.__init__(c=3) + assert mapping == {"c": 3} + assert obj.c == 3 + + +def test_namespace_replace_with_backing_dict(): + from types import SimpleNamespace + + class Namespace(SimpleNamespace): + def __init__(self): + self.seed = 1 + assert len(self.__dict__) == 1 + + obj = Namespace() + obj.extra = 2 + result = obj.__replace__(extra=3) + assert result.__dict__ == {"seed": 1, "extra": 3} + assert len(result.__dict__) == 2 + + +def test_object_dict_length_after_attribute_mutations(): + class Object: + pass + + obj = Object() + obj.deleted = 1 + del obj.deleted # Ordinary objects do not maintain HAS_NO_VALUE_PROPERTIES. + mapping = obj.__dict__ + assert len(mapping) == 0 + for _ in range(3): + obj.value = 1 + assert len(mapping) == 1 + obj.value = 2 + assert len(mapping) == 1 + del obj.value + assert len(mapping) == 0 + mapping.update(a=1, b=2) + assert len(mapping) == 2 + assert mapping.pop("a") == 1 + assert len(mapping) == 1 + mapping.clear() + assert len(mapping) == 0 + mapping = obj.__dict__ + + +def test_type_dict_length_and_dir_after_mutations(): + for size in (3, 1000): + for delete_before_dict in (False, True): + cls = type("ManyAttributes", (), {f"attr_{i}": i for i in range(size)}) + if delete_before_dict: + del cls.attr_0 + mapping = cls.__dict__ + expected = set(mapping) + for _ in range(3): + assert len(mapping) == len(expected) + assert set(dir(cls)) == expected | set(dir(object)) + cls.attr_0 = 42 + expected.add("attr_0") + assert len(mapping) == len(expected) + assert "attr_0" in dir(cls) + del cls.attr_0 + expected.remove("attr_0") + assert len(mapping) == len(expected) + assert "attr_0" not in dir(cls) + copied = mapping.copy() + assert len(copied) == len(expected) + assert set(copied) == expected + + +@graalpy_only +def test_dynamic_storage_cached_length_mutations(): + import __graalpython__ + + mapping = __graalpython__.set_storage_strategy({}, "dynamicobject") + assert len(mapping) == 0 + for _ in range(3): + mapping["a"] = 1 + assert len(mapping) == 1 + mapping["a"] = 2 + assert len(mapping) == 1 + del mapping["a"] + assert len(mapping) == 0 + mapping.setdefault("a", 3) + assert len(mapping) == 1 + mapping.update(b=4) + assert len(mapping) == 2 + assert mapping.pop("a") == 3 + assert len(mapping) == 1 + copied = mapping.copy() + assert len(copied) == 1 + copied["c"] = 5 + assert len(copied) == 2 + assert len(mapping) == 1 + mapping.clear() + assert len(mapping) == 0 + + def assert_raises(err, fn, *args, **kwargs): raised = False try: diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Python3Core.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Python3Core.java index 3cad68a2e7..b3c661f216 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Python3Core.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Python3Core.java @@ -396,7 +396,7 @@ import com.oracle.graal.python.nodes.BuiltinNames; import com.oracle.graal.python.nodes.PConstructAndRaiseNode; import com.oracle.graal.python.nodes.attributes.ReadAttributeFromPythonObjectNode; -import com.oracle.graal.python.nodes.attributes.WriteAttributeToPythonObjectNode; +import com.oracle.graal.python.nodes.attributes.WriteAttributeToObjectNode; import com.oracle.graal.python.nodes.call.CallDispatchers; import com.oracle.graal.python.nodes.object.GetForeignObjectClassNode; import com.oracle.graal.python.nodes.statement.AbstractImportNode; @@ -974,7 +974,7 @@ private void initializeImportlib() { boolean useFrozenModules = bootstrap != null; PyObjectCallMethodObjArgs callNode = PyObjectCallMethodObjArgs.getUncached(); - WriteAttributeToPythonObjectNode writeNode = WriteAttributeToPythonObjectNode.getUncached(); + WriteAttributeToObjectNode writeNode = WriteAttributeToObjectNode.getUncached(); ReadAttributeFromPythonObjectNode readNode = ReadAttributeFromPythonObjectNode.getUncached(); PyDictSetItem setItem = PyDictSetItem.getUncached(); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ImpModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ImpModuleBuiltins.java index 6b302d72eb..4af976d4fd 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ImpModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ImpModuleBuiltins.java @@ -98,7 +98,7 @@ import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.PConstructAndRaiseNode; import com.oracle.graal.python.nodes.PRaiseNode; -import com.oracle.graal.python.nodes.attributes.WriteAttributeToPythonObjectNode; +import com.oracle.graal.python.nodes.attributes.WriteAttributeToObjectNode; import com.oracle.graal.python.nodes.call.CallDispatchers; import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.PythonBuiltinNode; @@ -627,13 +627,13 @@ public static PythonModule importFrozenModuleObject(Node inliningTarget, PConstr if (info.isPackage) { /* Set __path__ to the empty list */ - WriteAttributeToPythonObjectNode.getUncached().execute(module, T___PATH__, PFactory.createList(core.getLanguage())); + WriteAttributeToObjectNode.getUncached().execute(module, T___PATH__, PFactory.createList(core.getLanguage())); } CallDispatchers.SimpleIndirectInvokeNode.executeUncached(code.getRootCallTarget(), PArguments.withGlobals(code, module)); Object origName = info.origName == null ? PNone.NONE : info.origName; - WriteAttributeToPythonObjectNode.getUncached().execute(module, T___ORIGNAME__, origName); + WriteAttributeToObjectNode.getUncached().execute(module, T___ORIGNAME__, origName); return module; } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java index 4bd4af4d7e..4874745d54 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java @@ -144,7 +144,6 @@ import com.oracle.graal.python.builtins.objects.module.PythonModule; import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.builtins.objects.tuple.PTuple; -import com.oracle.graal.python.builtins.objects.type.PythonBuiltinClass; import com.oracle.graal.python.builtins.objects.type.PythonManagedClass; import com.oracle.graal.python.builtins.objects.type.TpSlots; import com.oracle.graal.python.builtins.objects.type.TypeNodes; @@ -156,8 +155,6 @@ import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.nodes.argument.keywords.ExpandKeywordStarargsNode; import com.oracle.graal.python.nodes.argument.positional.ExecutePositionalStarargsNode; -import com.oracle.graal.python.nodes.attributes.WriteAttributeToObjectNode; -import com.oracle.graal.python.nodes.attributes.WriteAttributeToPythonObjectNode; import com.oracle.graal.python.nodes.classes.IsSubtypeNode; import com.oracle.graal.python.nodes.frame.GetCurrentFrameRef; import com.oracle.graal.python.nodes.object.GetClassNode; @@ -858,31 +855,6 @@ static long GraalPyPrivate_Type(long typeNamePtr) { throw PRaiseNode.raiseStatic(null, PythonErrorType.KeyError, ErrorMessages.APOSTROPHE_S, typeName); } - @GenerateInline - @GenerateCached(false) - abstract static class PyObjectSetAttrNode extends PNodeWithContext { - - abstract void execute(Node inliningTarget, Object object, TruffleString key, Object value); - - @Specialization - static void doBuiltinClass(PythonBuiltinClass object, TruffleString key, Object value, - @Exclusive @Cached WriteAttributeToObjectNode writeAttrNode) { - writeAttrNode.execute(object, key, value); - } - - @Specialization - static void doNativeClass(PythonNativeClass object, TruffleString key, Object value, - @Exclusive @Cached WriteAttributeToObjectNode writeAttrNode) { - writeAttrNode.execute(object, key, value); - } - - @Specialization(guards = {"!isPythonBuiltinClass(object)"}) - static void doObject(PythonObject object, TruffleString key, Object value, - @Exclusive @Cached WriteAttributeToPythonObjectNode writeAttrToPythonObjectNode) { - writeAttrToPythonObjectNode.execute(object, key, value); - } - } - @CApiBuiltin(ret = Void, args = {PyTypeObjectRawPointer}, call = Ignored) @TruffleBoundary static void GraalPyPrivate_AddInheritedSlots(long pythonClassPtr) { 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 59caca476c..dfc81cbba3 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 @@ -50,6 +50,7 @@ import com.oracle.graal.python.builtins.objects.dict.PDict; import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.builtins.objects.str.PString; +import com.oracle.graal.python.builtins.objects.type.PythonManagedClass; import com.oracle.graal.python.lib.PyObjectHashNode; import com.oracle.graal.python.lib.PyObjectRichCompareBool; import com.oracle.graal.python.lib.PyUnicodeCheckExactNode; @@ -76,6 +77,7 @@ import com.oracle.truffle.api.object.DynamicObject; import com.oracle.truffle.api.object.Shape; import com.oracle.truffle.api.profiles.InlinedConditionProfile; +import com.oracle.truffle.api.profiles.InlinedBranchProfile; import com.oracle.truffle.api.strings.TruffleString; /** @@ -93,6 +95,7 @@ public final class DynamicObjectStorage extends HashingStorage { public static final int EXPLODE_LOOP_SIZE_LIMIT = 16; final DynamicObject store; + int cachedLength = -1; static final class Store extends DynamicObject { public Store(Shape shape) { @@ -126,7 +129,37 @@ public abstract static class LengthNode extends Node { public abstract int execute(DynamicObjectStorage storage); - @Specialization(guards = {"cachedShape == self.store.getShape()", "keys.length < EXPLODE_LOOP_SIZE_LIMIT"}, limit = "2") + @Specialization(guards = "self.cachedLength >= 0") + static int cachedLength(DynamicObjectStorage self) { + assert self.cachedLength == actualLength(self) : "stale DynamicObjectStorage length"; + return self.cachedLength; + } + + @TruffleBoundary + private static int actualLength(DynamicObjectStorage self) { + int len = 0; + ReadAttributeFromPythonObjectNode readNode = ReadAttributeFromPythonObjectNode.getUncached(); + for (Object key : keyArray(self)) { + len = incrementLen(self, readNode, len, key); + } + return len; + } + + @Specialization(guards = {"self.cachedLength < 0", "hasNoDeletedProperties(self)"}) + static int shapeLength(DynamicObjectStorage self) { + return cacheLength(self, self.store.getShape().getPropertyCount()); + } + + static boolean hasNoDeletedProperties(DynamicObjectStorage self) { + return self.store instanceof PythonManagedClass && (self.store.getShape().getFlags() & PythonObject.HAS_NO_VALUE_PROPERTIES) == 0; + } + + private static int cacheLength(DynamicObjectStorage self, int length) { + self.cachedLength = length; + return length; + } + + @Specialization(guards = {"self.cachedLength < 0", "!hasNoDeletedProperties(self)", "cachedShape == self.store.getShape()", "keys.length < EXPLODE_LOOP_SIZE_LIMIT"}, limit = "2") @ExplodeLoop static int cachedLen(DynamicObjectStorage self, @SuppressWarnings("unused") @Cached("self.store.getShape()") Shape cachedShape, @@ -136,10 +169,10 @@ static int cachedLen(DynamicObjectStorage self, for (Object key : keys) { len = incrementLen(self, readNode, len, key); } - return len; + return cacheLength(self, len); } - @Specialization(replaces = "cachedLen") + @Specialization(guards = {"self.cachedLength < 0", "!hasNoDeletedProperties(self)"}, replaces = "cachedLen") static int length(DynamicObjectStorage self, @Shared @Cached(inline = false) ReadAttributeFromPythonObjectNode readNode, @Cached DynamicObject.GetKeyArrayNode keyArrayNode) { @@ -148,7 +181,7 @@ static int length(DynamicObjectStorage self, for (Object key : keys) { len = incrementLen(self, readNode, len, key); } - return len; + return cacheLength(self, len); } private static boolean hasStringKey(DynamicObjectStorage self, TruffleString key, ReadAttributeFromPythonObjectNode readNode) { @@ -269,14 +302,33 @@ private static boolean hasNext(Iterator keys) { } } - void setStringKey(TruffleString key, Object value, DynamicObject.PutNode putNode) { + void setStringKey(Node inliningTarget, TruffleString key, Object value, DynamicObject.PutNode putNode, + InlinedBranchProfile invalidateLengthProfile, DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + invalidateLength(inliningTarget, value, invalidateLengthProfile, setShapeFlagsNode); putNode.execute(store, key, assertNoJavaString(value)); } - boolean setStringKeyIfPresent(TruffleString key, Object value, DynamicObject.PutNode putNode) { + boolean setStringKeyIfPresent(Node inliningTarget, TruffleString key, Object value, DynamicObject.PutNode putNode, + InlinedBranchProfile invalidateLengthProfile, DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + invalidateLength(inliningTarget, value, invalidateLengthProfile, setShapeFlagsNode); return putNode.executeIfPresent(store, key, assertNoJavaString(value)); } + private void invalidateLength(Node inliningTarget, InlinedBranchProfile invalidateLengthProfile) { + if (cachedLength >= 0) { + invalidateLengthProfile.enter(inliningTarget); + cachedLength = -1; + } + } + + private void invalidateLength(Node inliningTarget, Object value, InlinedBranchProfile invalidateLengthProfile, DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + invalidateLength(inliningTarget, invalidateLengthProfile); + // Dictionary writes bypass WriteAttributeToObjectNode's maintenance of this flag. + if (value == PNone.NO_VALUE && store instanceof PythonManagedClass) { + setShapeFlagsNode.executeAdd(store, PythonObject.HAS_NO_VALUE_PROPERTIES); + } + } + boolean shouldTransitionOnPut() { // For now, we do not use SIZE_THRESHOLD condition to transition storages that wrap // dictionaries retrieved via object's __dict__ @@ -293,8 +345,10 @@ abstract static class ClearNode extends Node { public abstract HashingStorage execute(Node node, HashingStorage receiver); @Specialization(guards = "!isPythonObject(receiver.getStore())") - static HashingStorage clearPlain(DynamicObjectStorage receiver, + static HashingStorage clearPlain(Node inliningTarget, DynamicObjectStorage receiver, + @Cached InlinedBranchProfile invalidateLengthProfile, @Cached DynamicObject.ResetShapeNode resetShapeNode) { + receiver.invalidateLength(inliningTarget, invalidateLengthProfile); resetShapeNode.execute(receiver.getStore(), PythonLanguage.get(resetShapeNode).getEmptyShape()); return receiver; } @@ -341,8 +395,10 @@ 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) { - ((DynamicObjectStorage) self).setStringKey(key, value, putNode); + @Cached DynamicObject.PutNode putNode, + @Cached InlinedBranchProfile invalidateLengthProfile, + @Cached DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + ((DynamicObjectStorage) self).setStringKey(inliningTarget, key, value, putNode, invalidateLengthProfile, setShapeFlagsNode); } } } 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 11e066ccd6..4eaa77eb6c 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 @@ -328,9 +328,11 @@ static HashingStorage empty(Frame frame, Node inliningTarget, @SuppressWarnings( } @Specialization(guards = "!self.shouldTransitionOnPut()") - static HashingStorage domStringKey(DynamicObjectStorage self, TruffleString key, long keyHash, Object value, - @Cached DynamicObject.PutNode putNode) { - self.setStringKey(key, value, putNode); + static HashingStorage domStringKey(Node inliningTarget, DynamicObjectStorage self, TruffleString key, long keyHash, Object value, + @Cached DynamicObject.PutNode putNode, + @Cached InlinedBranchProfile invalidateLengthProfile, + @Cached DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + self.setStringKey(inliningTarget, key, value, putNode, invalidateLengthProfile, setShapeFlagsNode); return self; } @@ -379,8 +381,10 @@ public abstract HashingStorage execute(Frame frame, Node inliningTarget, Dynamic static HashingStorage domStringKey(Node inliningTarget, DynamicObjectStorage self, Object key, long keyHash, Object value, boolean transition, @SuppressWarnings("unused") @Cached PyUnicodeCheckExactNode isBuiltinString, @Cached CastBuiltinStringToTruffleStringNode castStr, - @Cached DynamicObject.PutNode putNode) { - self.setStringKey(castStr.execute(inliningTarget, key), value, putNode); + @Cached DynamicObject.PutNode putNode, + @Cached InlinedBranchProfile invalidateLengthProfile, + @Shared("setShapeFlags") @Cached DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + self.setStringKey(inliningTarget, castStr.execute(inliningTarget, key), value, putNode, invalidateLengthProfile, setShapeFlagsNode); return self; } @@ -389,7 +393,7 @@ static HashingStorage domTransition(Frame frame, Node inliningTarget, DynamicObj @Cached PyObjectHashNode hashNode, @Cached ObjectHashMap.PutNode putUnsafeNode, @Cached PutNode putNode, - @Cached DynamicObject.SetShapeFlagsNode setShapeFlags, + @Shared("setShapeFlags") @Cached DynamicObject.SetShapeFlagsNode setShapeFlags, @Cached DynamicObject.GetKeyArrayNode getKeyArrayNode, @Cached DynamicObject.GetNode getNode) { EconomicMapStorage result = dynamicObjectStorageToEconomicMap(inliningTarget, self, setShapeFlags, getKeyArrayNode, getNode, hashNode, putUnsafeNode); @@ -453,9 +457,11 @@ static HashingStorage empty(Frame frame, Node inliningTarget, @SuppressWarnings( } @Specialization(guards = "!self.shouldTransitionOnPut()") - static HashingStorage domStringKey(DynamicObjectStorage self, TruffleString key, Object value, - @Cached DynamicObject.PutNode putNode) { - self.setStringKey(key, value, putNode); + static HashingStorage domStringKey(Node inliningTarget, DynamicObjectStorage self, TruffleString key, Object value, + @Cached DynamicObject.PutNode putNode, + @Cached InlinedBranchProfile invalidateLengthProfile, + @Cached DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + self.setStringKey(inliningTarget, key, value, putNode, invalidateLengthProfile, setShapeFlagsNode); return self; } @@ -509,8 +515,10 @@ public abstract HashingStorage execute(Frame frame, Node inliningTarget, Dynamic static HashingStorage domStringKey(Node inliningTarget, DynamicObjectStorage self, Object key, Object value, boolean transition, @SuppressWarnings("unused") @Cached PyUnicodeCheckExactNode isBuiltinString, @Cached DynamicObject.PutNode putNode, - @Cached CastBuiltinStringToTruffleStringNode castStr) { - self.setStringKey(castStr.execute(inliningTarget, key), value, putNode); + @Cached CastBuiltinStringToTruffleStringNode castStr, + @Cached InlinedBranchProfile invalidateLengthProfile, + @Shared("setShapeFlags") @Cached DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { + self.setStringKey(inliningTarget, castStr.execute(inliningTarget, key), value, putNode, invalidateLengthProfile, setShapeFlagsNode); return self; } @@ -519,7 +527,7 @@ static HashingStorage domTransition(Frame frame, Node inliningTarget, DynamicObj @Cached PyObjectHashNode hashNode, @Cached ObjectHashMap.PutNode putUnsafeNode, @Cached PutNode putNode, - @Cached DynamicObject.SetShapeFlagsNode setShapeFlags, + @Shared("setShapeFlags") @Cached DynamicObject.SetShapeFlagsNode setShapeFlags, @Cached DynamicObject.GetKeyArrayNode getKeyArrayNode, @Cached DynamicObject.GetNode getNode) { EconomicMapStorage result = dynamicObjectStorageToEconomicMap(inliningTarget, self, setShapeFlags, getKeyArrayNode, getNode, hashNode, putUnsafeNode); @@ -594,7 +602,9 @@ static Object domStringKey(Frame frame, Node inliningTarget, DynamicObjectStorag @Cached CastBuiltinStringToTruffleStringNode castStr, @Exclusive @Cached PyObjectHashNode hashNode, @Cached DynamicObject.GetNode getNode, - @Cached DynamicObject.PutNode putNode) { + @Cached DynamicObject.PutNode putNode, + @Exclusive @Cached InlinedBranchProfile invalidateLengthProfile, + @Cached DynamicObject.SetShapeFlagsNode setShapeFlagsNode) { if (!isBuiltinString.execute(inliningTarget, keyObj)) { // Just for the potential side effects hashNode.execute(frame, inliningTarget, keyObj); @@ -607,11 +617,11 @@ static Object domStringKey(Frame frame, Node inliningTarget, DynamicObjectStorag if (val == PNone.NO_VALUE) { return null; } else { - self.setStringKey(key, PNone.NO_VALUE, putNode); + self.setStringKey(inliningTarget, key, PNone.NO_VALUE, putNode, invalidateLengthProfile, setShapeFlagsNode); return val; } } else { - return self.setStringKeyIfPresent(key, PNone.NO_VALUE, putNode); + return self.setStringKeyIfPresent(inliningTarget, key, PNone.NO_VALUE, putNode, invalidateLengthProfile, setShapeFlagsNode); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/namespace/SimpleNamespaceBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/namespace/SimpleNamespaceBuiltins.java index bd1ee8ba53..cdc8d373fa 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/namespace/SimpleNamespaceBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/namespace/SimpleNamespaceBuiltins.java @@ -88,7 +88,7 @@ import com.oracle.graal.python.nodes.PGuards; import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.nodes.attributes.ReadAttributeFromPythonObjectNode; -import com.oracle.graal.python.nodes.attributes.WriteAttributeToPythonObjectNode; +import com.oracle.graal.python.nodes.attributes.WriteAttributeToObjectNode; import com.oracle.graal.python.nodes.call.CallNode; import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode; @@ -149,7 +149,7 @@ static Object init(VirtualFrame frame, PSimpleNamespace self, Object[] args, PKe @Cached HashingStorageIteratorKey iteratorKey, @Cached HashingStorageIteratorValue iteratorValue, @Cached CastToTruffleStringNode castString, - @Cached WriteAttributeToPythonObjectNode writeAttrNode, + @Cached WriteAttributeToObjectNode writeAttrNode, @Cached PRaiseNode raiseNode) { if (args.length > 1) { throw raiseNode.raise(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.EXPECTED_AT_MOST_ONE_ARG_GOT_D, @@ -238,7 +238,7 @@ static Object replace(VirtualFrame frame, PSimpleNamespace self, @SuppressWarnin @Cached CallNode callNode, @Cached DynamicObject.GetKeyArrayNode getKeyArrayNode, @Cached(inline = true) ReadAttributeFromPythonObjectNode readAttrNode, - @Cached WriteAttributeToPythonObjectNode writeAttrNode, + @Cached WriteAttributeToObjectNode writeAttrNode, @Cached PRaiseNode raiseNode) { if (args.length > 0) { throw raiseNode.raise(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.SIMPLE_NAMESPACE_REPLACE_NO_POSITIONAL); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/WriteAttributeToPythonObjectNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/WriteAttributeToPythonObjectNode.java index 68ce0ac525..3897977ea9 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/WriteAttributeToPythonObjectNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/WriteAttributeToPythonObjectNode.java @@ -42,6 +42,7 @@ import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.nodes.PNodeWithContext; +import com.oracle.graal.python.nodes.object.GetDictIfExistsNode; import com.oracle.graal.python.runtime.PythonOptions; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; @@ -54,10 +55,9 @@ import com.oracle.truffle.api.strings.TruffleString; /** - * Writes attribute directly to the underlying {@link DynamicObject} regardless of whether the - * object has dict, also bypasses any other additional logic in {@link WriteAttributeToObjectNode}. - * This node does not provide any functionality on top of - * {@link com.oracle.truffle.api.object.DynamicObject.PutNode}, its purpose is to provide an + * Writes an attribute directly to the underlying {@link DynamicObject}. The caller must ensure + * that the object has no backing dictionary. Otherwise, use {@link WriteAttributeToObjectNode}. + * This node bypasses the additional logic in that node. Its purpose is to provide an * abstraction in preparation for the transition from {@link DynamicObject} to * {@link com.oracle.graal.python.builtins.objects.common.ObjectHashMap}. */ @@ -85,6 +85,7 @@ public static WriteAttributeToPythonObjectNode getUncached() { @Specialization static void write(PythonObject dynamicObject, TruffleString key, Object value, @Cached DynamicObject.PutNode putNode) { + assert GetDictIfExistsNode.getDictUncached(dynamicObject) == null : "direct attribute write with a backing dictionary"; putNode.execute(dynamicObject, key, value); } } From 6d7e39bcd48a11ca15ca716466569b4579c52e78 Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 15 Sep 2026 18:41:04 +0200 Subject: [PATCH 3/5] Guard direct attribute stores with object dictionaries --- .../graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 874704b242..5d6e168f35 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 @@ -2264,7 +2264,7 @@ public static PythonManagedClass getManagedClassOrNull(Shape cachedShape) { @Idempotent public static boolean hasNoSlotsOrMaterializedDict(Shape cachedShape) { - return (cachedShape.getFlags() & (PythonObject.HAS_MATERIALIZED_DICT | PythonObject.HAS_SLOTS_BUT_NO_DICT_FLAG)) == 0; + return (cachedShape.getFlags() & (PythonObject.HAS_DICT | PythonObject.HAS_SLOTS_BUT_NO_DICT_FLAG)) == 0; } @ForceQuickening From 421c712c971d3e866cea0647122564ac4f5603f6 Mon Sep 17 00:00:00 2001 From: stepan Date: Wed, 16 Sep 2026 14:25:17 +0200 Subject: [PATCH 4/5] Add regression test for fixed bug in SimpleNamespace --- .../src/tests/test_dict.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py b/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py index 0c35a6bcd7..b9cd65998f 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_dict.py @@ -60,6 +60,20 @@ def test_namespace_reinitialization_with_backing_dict(): assert obj.c == 3 +def test_namespace_reinitialization_with_positional_mapping_and_backing_dict(): + from types import SimpleNamespace + + obj = SimpleNamespace(a=1) + mapping = obj.__dict__ + assert len(mapping) == 1 + mapping.clear() + assert len(mapping) == 0 + obj.__init__({"b": 2}, c=3) + assert len(mapping) == 2 + assert mapping == {"b": 2, "c": 3} + assert obj.__dict__ is mapping + + def test_namespace_replace_with_backing_dict(): from types import SimpleNamespace From 7e09b88afeaffb7c71b3a6e0b504a07ad471ad9a Mon Sep 17 00:00:00 2001 From: stepan Date: Thu, 17 Sep 2026 09:15:57 +0200 Subject: [PATCH 5/5] Remove unused Checkstyle import --- .../graal/python/builtins/modules/cext/PythonCextBuiltins.java | 1 - 1 file changed, 1 deletion(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java index 4874745d54..49db8bed93 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java @@ -182,7 +182,6 @@ import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.dsl.Cached; -import com.oracle.truffle.api.dsl.Cached.Exclusive; import com.oracle.truffle.api.dsl.GenerateCached; import com.oracle.truffle.api.dsl.GenerateInline; import com.oracle.truffle.api.dsl.GenerateUncached;