From b46c73874231a3ba2797c15b4697e9212565499e Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 15 Sep 2026 18:27:50 +0200 Subject: [PATCH] Fast path for type.dir() --- .../src/tests/test_type.py | 30 +++++++++ .../builtins/objects/type/TypeBuiltins.java | 63 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_type.py b/graalpython/com.oracle.graal.python.test/src/tests/test_type.py index 1645c6b831..1cdb3a5899 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_type.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_type.py @@ -162,6 +162,7 @@ class MyStr(str): MyStr("x"): 42 }) assert any(type(k) == MyStr for k in A.__dict__.keys()) + assert any(type(k) == MyStr for k in type.__dir__(A)) def test_mro(): @@ -186,6 +187,35 @@ class C: assert dir(C()) == sorted(dir(C())) +def test_type_dir_many_string_attributes(): + base_attributes = {f"base_{i}": i for i in range(200)} + attributes = {f"attribute_{i}": i for i in range(1000)} + base = type("Base", (), base_attributes) + cls = type("ManyAttributes", (base,), attributes) + del cls.attribute_5 + expected = set(base.__dict__) | set(cls.__dict__) | set(object.__dict__) + assert set(type.__dir__(cls)) == expected + assert dir(cls) == sorted(expected) + + +def test_type_dir_custom_mappingproxy_falls_back(): + lookups = [] + + class Meta(type): + @property + def __dict__(self): + lookups.append(self) + return {"custom": 1} + + class C(metaclass=Meta): + hidden_by_metaclass = 1 + + result = type.__dir__(C) + assert "custom" in result + assert "hidden_by_metaclass" not in result + assert lookups == [C] + + def test_isinstance_non_type(): import typing assert isinstance(1, typing.AbstractSet) is False 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..ae8eedf0b1 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 @@ -77,6 +77,7 @@ import static com.oracle.graal.python.util.PythonUtils.tsLiteral; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import com.oracle.graal.python.PythonLanguage; @@ -180,6 +181,7 @@ import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.object.DynamicObject; import com.oracle.truffle.api.profiles.BranchProfile; import com.oracle.truffle.api.profiles.InlinedBranchProfile; import com.oracle.truffle.api.profiles.InlinedConditionProfile; @@ -1242,9 +1244,18 @@ public abstract static class DirNode extends PythonUnaryBuiltinNode { @Specialization static PList dir(VirtualFrame frame, Object klass, @Bind Node inliningTarget, + @Bind PythonLanguage language, + @Bind PythonContext context, @Cached ConstructListNode constructListNode, + @Cached InlinedBranchProfile slowPathBranch, @Cached("createFor($node)") BoundaryCallData boundaryCallData) { - PSet names = PFactory.createSet(PythonLanguage.get(inliningTarget)); + Object[] fastNames = dirFast(language, context, klass); + if (fastNames != null) { + return PFactory.createList(language, fastNames); + } + + slowPathBranch.enter(inliningTarget); + PSet names = PFactory.createSet(language); Object state = BoundaryCallContext.enter(frame, boundaryCallData); try { dir(names, klass); @@ -1254,6 +1265,56 @@ static PList dir(VirtualFrame frame, Object klass, return constructListNode.execute(frame, names); } + @TruffleBoundary + private static Object[] dirFast(PythonLanguage language, PythonContext context, Object klass) { + if (!isFastPathEligible(context, klass)) { + return null; + } + LinkedHashSet names = new LinkedHashSet<>(); + collectDynamicObjectStorageKeys(names, context, klass); + return names.toArray(); + } + + private static boolean isFastPathEligible(PythonContext context, Object klass) { + PythonManagedClass managedClass = asManagedClass(context, klass); + if (managedClass == null || PGuards.hasMaterializedDict(managedClass.getShape())) { + return false; + } + for (PythonAbstractClass base : managedClass.getBaseClasses()) { + if (!isFastPathEligible(context, base)) { + return false; + } + } + return true; + } + + private static PythonManagedClass asManagedClass(PythonContext context, Object klass) { + if (GetClassNode.executeUncached(klass) != PythonBuiltinClassType.PythonClass) { + return null; + } + if (klass instanceof PythonManagedClass pythonManagedClass) { + return pythonManagedClass; + } else if (klass instanceof PythonBuiltinClassType builtinClassType) { + return context.lookupType(builtinClassType); + } + return null; + } + + private static void collectDynamicObjectStorageKeys(LinkedHashSet names, PythonContext context, Object klass) { + PythonManagedClass managedClass = asManagedClass(context, klass); + assert managedClass != null && !PGuards.hasMaterializedDict(managedClass.getShape()); + DynamicObject.GetNode getNode = DynamicObject.GetNode.getUncached(); + for (Object key : DynamicObject.GetKeyArrayNode.getUncached().execute(managedClass)) { + if (key instanceof TruffleString stringKey && getNode.execute(managedClass, stringKey, NO_VALUE) != NO_VALUE) { + names.add(stringKey); + } + } + + for (PythonAbstractClass base : managedClass.getBaseClasses()) { + collectDynamicObjectStorageKeys(names, context, base); + } + } + @TruffleBoundary public static void dir(PSet names, Object klass) { Object ns = PyObjectLookupAttr.executeUncached(klass, T___DICT__);