diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_call-classmethod.py b/graalpython/com.oracle.graal.python.test/src/tests/test_call-classmethod.py index 043a01e544..2bf5e689eb 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_call-classmethod.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_call-classmethod.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018, 2021, Oracle and/or its affiliates. +# Copyright (c) 2018, 2026, Oracle and/or its affiliates. # Copyright (c) 2013, Regents of the University of California # # All rights reserved. @@ -47,3 +47,137 @@ def test_strength(): assert not Strength.weaker(s1, s2) assert s1.stronger(s1, s2) assert not s1.weaker(s1, s2) + + +def test_classmethod_wraps_descriptor(): + class Descriptor: + def __get__(self, obj, typ=None): + return obj, typ + + class C: + method = classmethod(Descriptor()) + + assert C.method == (C, C) + assert C().method == (C, C) + + method = C.__dict__["method"] + assert method.__get__(None, C) == (C, C) + assert method.__get__(C()) == (C, C) + + +def test_classmethod_wraps_property(): + class C: + @classmethod + @property + def name(cls): + return cls.__name__ + + class D(C): + pass + + assert C.name == "C" + assert C().name == "C" + assert D.name == "D" + assert D().name == "D" + + +def test_classmethod_wraps_staticmethod(): + class C: + method = classmethod(staticmethod(lambda value: ("static", value))) + + assert C.method("arg") == ("static", "arg") + assert C().method("arg") == ("static", "arg") + + +def test_classmethod_wraps_classmethod(): + class C: + def method(cls, value): + return cls, value + + method = classmethod(classmethod(method)) + + class D(C): + pass + + assert C.method("arg") == (C, "arg") + assert C().method("arg") == (C, "arg") + assert D.method("arg") == (D, "arg") + assert D().method("arg") == (D, "arg") + + +def test_classmethod_wraps_bound_method(): + class C: + def method(self, cls): + return self, cls + + receiver = C() + assert not hasattr(type(receiver.method), "__get__") + + class D: + method = classmethod(receiver.method) + + assert D.method() == (receiver, D) + assert D().method() == (receiver, D) + + +def test_classmethod_descriptor_get_errors(): + descriptor = dict.__dict__["fromkeys"] + + assert descriptor.__get__(None, dict)([1, 2]) == {1: None, 2: None} + assert descriptor.__get__({})([1, 2]) == {1: None, 2: None} + + for args in ((None, None), (42,), (None, 42), (None, int), ({}, int)): + try: + descriptor.__get__(*args) + except TypeError: + pass + else: + raise AssertionError("classmethod_descriptor.__get__ accepted invalid arguments") + + +def test_classmethod_descriptor_get_uses_object_type_when_type_omitted(): + class MyDict(dict): + pass + + descriptor = dict.__dict__["fromkeys"] + bound = descriptor.__get__(MyDict()) + assert bound.__self__ is MyDict + assert type(bound([1, 2])) is MyDict + + +def test_classmethod_descriptor_get_does_not_keep_type_alive(): + import time + from test import support + + descriptor = object.__dict__["__init_subclass__"] + + class Parent: + pass + + class Child(Parent): + pass + + bound = descriptor.__get__(None, Child) + del bound + assert Parent.__subclasses__() == [Child] + + del Child + for _ in range(100): + support.gc_collect() + if not Parent.__subclasses__(): + break + if getattr(support, "is_graalpy", False): + time.sleep(0.1) + assert Parent.__subclasses__() == [] + + +def test_cext_classmethod_descriptor(): + from _ctypes import _SimpleCData + + class c_void_p(_SimpleCData): + _type_ = "P" + + descriptor = c_void_p.__dict__["from_param"] + assert type(descriptor).__name__ == "classmethod_descriptor" + assert callable(descriptor.__get__(None, c_void_p)) + c_void_p.from_param(0) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_methods.py b/graalpython/com.oracle.graal.python.test/src/tests/test_methods.py index c27e3779a8..0f518fbadf 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_methods.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_methods.py @@ -107,3 +107,35 @@ def g(self): assert types.MethodType(f, A).__qualname__ == "test_method_qualname_uses_wrapped_callable..f" assert A.m.__qualname__ == "test_method_qualname_uses_wrapped_callable..f" assert A.__dict__["m"].__qualname__ == "test_method_qualname_uses_wrapped_callable..f" + + +def test_method_getattribute_does_not_swallow_method_descriptor_attribute_error(): + import types + + class Callable: + def __init__(self): + self.name_calls = 0 + + def __call__(self, *args): + pass + + def __getattribute__(self, name): + if name == "__name__": + calls = object.__getattribute__(self, "name_calls") + object.__setattr__(self, "name_calls", calls + 1) + if calls == 0: + raise AttributeError("first lookup failure") + return "fallback-name" + return object.__getattribute__(self, name) + + func = Callable() + method = types.MethodType(func, object()) + try: + method.__name__ + except AttributeError as e: + assert str(e) == "first lookup failure" + else: + assert False, "AttributeError was not raised" + assert func.name_calls == 1 + assert method.__class__ is types.MethodType + assert method.__name__ == "fallback-name" diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDescrBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDescrBuiltins.java index f6b55a4598..12e097e065 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDescrBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDescrBuiltins.java @@ -104,7 +104,7 @@ public static long GraalPyPrivate_Descr_NewClassMethod(long methodDefPtr, long n Object type = NativeToPythonClassInternalNode.executeUncached(typeRaw); PBuiltinFunction func = MethodDescriptorWrapper.createWrapperFunction(language, name, methPtr, type, flags); assert func != null; - PDecoratedMethod classMethod = PFactory.createClassmethodFromCallableObj(language, func); + PDecoratedMethod classMethod = PFactory.createBuiltinClassmethodFromCallableObj(language, func); WriteAttributeToPythonObjectNode.executeUncached(classMethod, T___NAME__, name); WriteAttributeToPythonObjectNode.executeUncached(classMethod, T___DOC__, doc); HiddenAttr.WriteLongNode.executeUncached(classMethod, METHOD_DEF_PTR, methodDefPtr); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java index cf0be21c8f..908ad9da5d 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java @@ -298,7 +298,7 @@ private static PythonBuiltinObject typeAddMethod(PythonLanguage language, long m throw PRaiseNode.raiseStatic(EncapsulatingNodeReference.getCurrent().get(), PythonBuiltinClassType.ValueError, ErrorMessages.METHOD_CANNOT_BE_BOTH_CLASS_AND_STATIC); } assert func != null; - return PFactory.createClassmethodFromCallableObj(language, func); + return PFactory.createBuiltinClassmethodFromCallableObj(language, func); } else if (CExtContext.isMethStatic(flags)) { return PFactory.createStaticmethodFromCallableObj(language, func); } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/BuiltinClassmethodBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/BuiltinClassmethodBuiltins.java index 34a8e80445..9dc0b3832e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/BuiltinClassmethodBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/BuiltinClassmethodBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 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 @@ -61,21 +61,35 @@ import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.PythonBuiltins; import com.oracle.graal.python.builtins.objects.PNone; +import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction; import com.oracle.graal.python.builtins.objects.str.StringUtils.SimpleTruffleStringFormatNode; import com.oracle.graal.python.builtins.objects.type.TpSlots; import com.oracle.graal.python.builtins.objects.type.TypeNodes; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.DescrGetBuiltinNode; import com.oracle.graal.python.lib.PyObjectGetAttr; import com.oracle.graal.python.lib.PyObjectLookupAttr; import com.oracle.graal.python.lib.PyObjectStrAsTruffleStringNode; +import com.oracle.graal.python.nodes.ErrorMessages; +import com.oracle.graal.python.nodes.PGuards; +import com.oracle.graal.python.nodes.PRaiseNode; +import com.oracle.graal.python.nodes.classes.IsSubtypeNode; import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode; +import com.oracle.graal.python.nodes.object.GetClassNode; +import com.oracle.graal.python.runtime.exception.PException; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +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.GenerateNodeFactory; +import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.dsl.ReportPolymorphism; 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.profiles.InlinedBranchProfile; import com.oracle.truffle.api.strings.TruffleString; @CoreFunctions(extendClasses = PythonBuiltinClassType.PBuiltinClassMethod) @@ -88,6 +102,97 @@ protected List> getNodeFa return BuiltinClassmethodBuiltinsFactory.getFactories(); } + @Slot(SlotKind.tp_descr_get) + @ReportPolymorphism + @GenerateUncached + @GenerateNodeFactory + abstract static class GetNode extends DescrGetBuiltinNode { + // If self.getCallable() is null, let the next @Specialization handle that + @Specialization(guards = {"isSingleContext()", "isNoValue(type) == typeIsNoValue", "cachedSelf == self", "cachedCallable != null"}, limit = "3") + static Object getCached(@SuppressWarnings("unused") PDecoratedMethod self, Object obj, Object type, + @Bind Node inliningTarget, + @SuppressWarnings("unused") @Cached(value = "self", weak = true) PDecoratedMethod cachedSelf, + @Cached(value = "self.getCallable()", weak = true) Object cachedCallable, + @Cached("isNoValue(type)") boolean typeIsNoValue, + @Exclusive @Cached GetClassNode getClass, + @Exclusive @Cached IsSubtypeNode isSubtypeNode, + @Exclusive @Cached InlinedBranchProfile errorProfile, + @Exclusive @Cached ClassmethodCommonBuiltins.MakeMethodNode makeMethod) { + Object actualType = getType(inliningTarget, errorProfile, getClass, cachedCallable, typeIsNoValue, obj, type); + return doGet(inliningTarget, isSubtypeNode, errorProfile, makeMethod, actualType, cachedCallable); + } + + @InliningCutoff + @Specialization(replaces = "getCached") + static Object get(PDecoratedMethod self, Object obj, Object type, + @Bind Node inliningTarget, + @Exclusive @Cached GetClassNode getClass, + @Exclusive @Cached IsSubtypeNode isSubtypeNode, + @Exclusive @Cached InlinedBranchProfile errorProfile, + @Exclusive @Cached ClassmethodCommonBuiltins.MakeMethodNode makeMethod, + @Exclusive @Cached PRaiseNode raiseNode) { + Object callable = ClassmethodCommonBuiltins.getCallable(inliningTarget, self, raiseNode); + Object actualType = getType(inliningTarget, errorProfile, getClass, callable, PGuards.isNoValue(type), obj, type); + return doGet(inliningTarget, isSubtypeNode, errorProfile, makeMethod, actualType, callable); + } + + private static Object getType(Node inliningTarget, InlinedBranchProfile errorProfile, GetClassNode getClassNode, + Object callable, boolean typeIsNoValue, Object obj, Object type) { + if (typeIsNoValue) { + if (PGuards.isNoValue(obj)) { + errorProfile.enter(inliningTarget); + throw raiseNeedsEitherObjOrType(inliningTarget, callable); + } + return getClassNode.execute(inliningTarget, obj); + } + return type; + } + + @TruffleBoundary + private static PException raiseNeedsEitherObjOrType(Node inliningTarget, Object callable) { + if (callable instanceof PBuiltinFunction pbf) { + throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, + ErrorMessages.DESCRIPTOR_S_FOR_TYPE_S_NEEDS_EITHER_OBJ_OR_TYPE, pbf.getName(), + TypeNodes.GetNameNode.executeUncached(pbf.getEnclosingType())); + } else { + throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError); + } + } + + private static Object doGet(Node inliningTarget, IsSubtypeNode isSubtypeNode, InlinedBranchProfile errorProfile, + ClassmethodCommonBuiltins.MakeMethodNode makeMethod, Object type, Object callable) { + if (!PGuards.isPythonClass(type)) { + errorProfile.enter(inliningTarget); + throw raiseNeedsType(inliningTarget, callable, type); + } + // Not clear if we can get any other callable than PBuiltinFunction... + if (callable instanceof PBuiltinFunction builtinFunction) { + Object descriptorType = builtinFunction.getEnclosingType(); + if (!isSubtypeNode.execute(type, descriptorType)) { + errorProfile.enter(inliningTarget); + throw raiseRequiresSubtype(inliningTarget, builtinFunction, descriptorType, type); + } + } + return makeMethod.execute(inliningTarget, type, callable); + } + + @TruffleBoundary + private static RuntimeException raiseNeedsType(Node inliningTarget, Object callable, Object type) { + if (callable instanceof PBuiltinFunction pbf) { + throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.DESCRIPTOR_S_FOR_TYPE_S_NEEDS_TYPE_NOT_P_AS_ARG_2, + pbf.getName(), TypeNodes.GetNameNode.executeUncached(pbf.getEnclosingType()), type); + } else { + throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError); + } + } + + @TruffleBoundary + private static RuntimeException raiseRequiresSubtype(Node inliningTarget, PBuiltinFunction builtinFunction, Object descriptorType, Object type) { + throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.DESCRIPTOR_S_REQUIRES_SUBTYPE_OF_S_BUT_RECEIVED_S, + builtinFunction.getName(), TypeNodes.GetNameNode.executeUncached(descriptorType), TypeNodes.GetNameNode.executeUncached(type)); + } + } + @Builtin(name = J___NAME__, maxNumOfPositionalArgs = 1, isGetter = true) @GenerateNodeFactory abstract static class NameNode extends PythonUnaryBuiltinNode { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodBuiltins.java index 36aa0478a9..c6c0b5854f 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -54,18 +54,29 @@ import com.oracle.graal.python.builtins.PythonBuiltins; import com.oracle.graal.python.builtins.objects.PNone; import com.oracle.graal.python.builtins.objects.type.TpSlots; +import com.oracle.graal.python.builtins.objects.type.TpSlots.GetObjectSlotsNode; import com.oracle.graal.python.builtins.objects.type.TypeNodes; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlot; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.CallSlotDescrGet; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.DescrGetBuiltinNode; import com.oracle.graal.python.lib.PyObjectLookupAttr; import com.oracle.graal.python.lib.PyObjectReprAsTruffleStringNode; import com.oracle.graal.python.lib.PyObjectSetAttr; +import com.oracle.graal.python.nodes.PGuards; +import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.builtins.PythonBinaryBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode; +import com.oracle.graal.python.nodes.object.GetClassNode; import com.oracle.graal.python.runtime.object.PFactory; +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.GenerateNodeFactory; +import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.dsl.ReportPolymorphism; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.Node; @@ -109,6 +120,55 @@ protected PNone init(VirtualFrame frame, PDecoratedMethod self, Object callable, } } + @Slot(value = SlotKind.tp_descr_get, isComplex = true) + @ReportPolymorphism + @GenerateUncached + @GenerateNodeFactory + abstract static class GetNode extends DescrGetBuiltinNode { + // If self.getCallable() is null, let the next @Specialization handle that + @Specialization(guards = {"isSingleContext()", "isNoValue(type) == typeIsNoValue", "cachedSelf == self", "cachedCallable != null"}, limit = "3") + static Object getCached(VirtualFrame frame, @SuppressWarnings("unused") PDecoratedMethod self, Object obj, Object type, + @Bind Node inliningTarget, + @SuppressWarnings("unused") @Cached(value = "self", weak = true) PDecoratedMethod cachedSelf, + @Cached(value = "self.getCallable()", weak = true) Object cachedCallable, + @Cached("isNoValue(type)") boolean typeIsNoValue, + @Exclusive @Cached GetClassNode getClass, + @Exclusive @Cached GetObjectSlotsNode getCallableSlots, + @Exclusive @Cached CallSlotDescrGet callGet, + @Exclusive @Cached ClassmethodCommonBuiltins.MakeMethodNode makeMethod) { + Object actualType = typeIsNoValue ? getClass.execute(inliningTarget, obj) : type; + return doGet(frame, inliningTarget, actualType, cachedCallable, getCallableSlots, callGet, makeMethod); + } + + @InliningCutoff + @Specialization(replaces = "getCached") + static Object get(VirtualFrame frame, PDecoratedMethod self, Object obj, Object type, + @Bind Node inliningTarget, + @Exclusive @Cached GetClassNode getClass, + @Exclusive @Cached GetObjectSlotsNode getCallableSlots, + @Exclusive @Cached CallSlotDescrGet callGet, + @Exclusive @Cached ClassmethodCommonBuiltins.MakeMethodNode makeMethod, + @Exclusive @Cached PRaiseNode raiseNode) { + Object actualType = PGuards.isNoValue(type) ? getClass.execute(inliningTarget, obj) : type; + return doGet(frame, inliningTarget, actualType, ClassmethodCommonBuiltins.getCallable(inliningTarget, self, raiseNode), + getCallableSlots, callGet, makeMethod); + } + + private static Object doGet(VirtualFrame frame, Node inliningTarget, Object type, Object callable, + GetObjectSlotsNode getCallableSlots, CallSlotDescrGet callGet, ClassmethodCommonBuiltins.MakeMethodNode makeMethod) { + TpSlot get = getCallableSlots.execute(inliningTarget, callable).tp_descr_get(); + if (get != null) { + return callGet(frame, inliningTarget, type, callable, callGet, get); + } + return makeMethod.execute(inliningTarget, type, callable); + } + + @InliningCutoff + private static Object callGet(VirtualFrame frame, Node inliningTarget, Object type, Object callable, CallSlotDescrGet callGet, TpSlot get) { + return callGet.execute(frame, inliningTarget, get, callable, type, type); + } + } + @Slot(value = SlotKind.tp_repr, isComplex = true) @GenerateNodeFactory abstract static class ReprNode extends PythonUnaryBuiltinNode { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodCommonBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodCommonBuiltins.java index d733da5541..5f7ff095ba 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodCommonBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/ClassmethodCommonBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -53,18 +53,14 @@ import com.oracle.graal.python.builtins.objects.function.PFunction; import com.oracle.graal.python.builtins.objects.function.PKeyword; import com.oracle.graal.python.builtins.objects.type.TpSlots; -import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.DescrGetBuiltinNode; import com.oracle.graal.python.nodes.ErrorMessages; 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.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.builtins.PythonVarargsBuiltinNode; -import com.oracle.graal.python.nodes.object.GetClassNode; import com.oracle.graal.python.runtime.object.PFactory; 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.GenerateNodeFactory; @@ -86,66 +82,12 @@ protected List> getNodeFa return ClassmethodCommonBuiltinsFactory.getFactories(); } - @Slot(SlotKind.tp_descr_get) - @ReportPolymorphism - @GenerateUncached - @GenerateNodeFactory - abstract static class GetNode extends DescrGetBuiltinNode { - /*- - TODO: (GR-53082) this is not handling following code-path added to CPython at some later point: - if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) { - return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type, type); - } - - Additionally, in CPython tp_descrget is not shared between classmethod_descriptor and classmethod, - we should investigate if we can really share the implementation - */ - - // If self.getCallable() is null, let the next @Specialization handle that - @Specialization(guards = {"isSingleContext()", "isNoValue(type)", "cachedSelf == self", "cachedCallable != null"}, limit = "3") - static Object getCached(@SuppressWarnings("unused") PDecoratedMethod self, Object obj, @SuppressWarnings("unused") Object type, - @Bind Node inliningTarget, - @SuppressWarnings("unused") @Cached(value = "self", weak = true) PDecoratedMethod cachedSelf, - @SuppressWarnings("unused") @Cached(value = "self.getCallable()", weak = true) Object cachedCallable, - @Shared @Cached GetClassNode getClass, - @Shared @Cached MakeMethodNode makeMethod) { - return makeMethod.execute(inliningTarget, getClass.execute(inliningTarget, obj), cachedCallable); - } - - @Specialization(guards = "isNoValue(type)", replaces = "getCached") - static Object get(PDecoratedMethod self, Object obj, @SuppressWarnings("unused") Object type, - @Bind Node inliningTarget, - @Shared @Cached GetClassNode getClass, - @Shared @Cached MakeMethodNode makeMethod, - @Shared @Cached PRaiseNode raiseNode) { - return doGet(inliningTarget, self, getClass.execute(inliningTarget, obj), makeMethod, raiseNode); - } - - // If self.getCallable() is null, let the next @Specialization handle that - @Specialization(guards = {"isSingleContext()", "!isNoValue(type)", "cachedSelf == self", "cachedCallable != null"}, limit = "3") - static Object getTypeCached(@SuppressWarnings("unused") PDecoratedMethod self, @SuppressWarnings("unused") Object obj, Object type, - @Bind Node inliningTarget, - @SuppressWarnings("unused") @Cached(value = "self", weak = true) PDecoratedMethod cachedSelf, - @SuppressWarnings("unused") @Cached(value = "self.getCallable()", weak = true) Object cachedCallable, - @Shared @Cached MakeMethodNode makeMethod) { - return makeMethod.execute(inliningTarget, type, cachedCallable); - } - - @Specialization(guards = "!isNoValue(type)", replaces = "getTypeCached") - static Object getType(PDecoratedMethod self, @SuppressWarnings("unused") Object obj, Object type, - @Bind Node inliningTarget, - @Shared @Cached MakeMethodNode makeMethod, - @Shared @Cached PRaiseNode raiseNode) { - return doGet(inliningTarget, self, type, makeMethod, raiseNode); - } - - private static Object doGet(Node inliningTarget, PDecoratedMethod self, Object type, MakeMethodNode makeMethod, PRaiseNode raiseNode) { - Object callable = self.getCallable(); - if (callable == null) { - throw raiseNode.raise(inliningTarget, PythonBuiltinClassType.RuntimeError, ErrorMessages.UNINITIALIZED_S_OBJECT); - } - return makeMethod.execute(inliningTarget, type, callable); + static Object getCallable(Node inliningTarget, PDecoratedMethod self, PRaiseNode raiseNode) { + Object callable = self.getCallable(); + if (callable == null) { + throw raiseNode.raise(inliningTarget, PythonBuiltinClassType.RuntimeError, ErrorMessages.UNINITIALIZED_S_OBJECT); } + return callable; } @GenerateInline diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/MethodBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/MethodBuiltins.java index e190b6b021..6321bb0304 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/MethodBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/MethodBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. * Copyright (c) 2014, Regents of the University of California * * All rights reserved. @@ -51,10 +51,11 @@ import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction; import com.oracle.graal.python.builtins.objects.function.PFunction; import com.oracle.graal.python.builtins.objects.function.PKeyword; -import com.oracle.graal.python.builtins.objects.object.ObjectBuiltins; +import com.oracle.graal.python.builtins.objects.str.StringNodes; import com.oracle.graal.python.builtins.objects.str.StringUtils.SimpleTruffleStringFormatNode; import com.oracle.graal.python.builtins.objects.type.TpSlots; -import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.DescrGetBuiltinNode; +import com.oracle.graal.python.builtins.objects.type.TpSlots.GetObjectSlotsNode; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.CallSlotDescrGet; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotGetAttr.GetAttrBuiltinNode; import com.oracle.graal.python.lib.PyCallableCheckNode; import com.oracle.graal.python.lib.PyObjectGetAttr; @@ -63,23 +64,22 @@ import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.PGuards; import com.oracle.graal.python.nodes.PRaiseNode; +import com.oracle.graal.python.nodes.attributes.LookupAttributeInMRONode; import com.oracle.graal.python.nodes.builtins.FunctionNodes.GetDefaultsNode; import com.oracle.graal.python.nodes.builtins.FunctionNodes.GetKeywordDefaultsNode; import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.PythonBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonTernaryBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode; -import com.oracle.graal.python.nodes.object.BuiltinClassProfiles.IsBuiltinObjectProfile; +import com.oracle.graal.python.nodes.object.GetClassNode; 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.exception.PException; import com.oracle.graal.python.runtime.object.PFactory; 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.GenerateNodeFactory; -import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.NodeFactory; import com.oracle.truffle.api.dsl.Specialization; @@ -164,25 +164,22 @@ public abstract static class GetattributeNode extends GetAttrBuiltinNode { @Specialization static Object doIt(VirtualFrame frame, PMethod self, Object keyObj, @Bind Node inliningTarget, - @Cached ObjectBuiltins.GetAttributeNode objectGetattrNode, - @Cached IsBuiltinObjectProfile errorProfile, - @Cached CastToTruffleStringNode castKeyToStringNode, - @Cached PRaiseNode raiseNode) { - // TODO: (GR-53090) this is different to what CPython does and CPython also does not - // define tp_descrget for method - TruffleString key; - try { - key = castKeyToStringNode.execute(inliningTarget, keyObj); - } catch (CannotCastException e) { - throw raiseNode.raise(inliningTarget, TypeError, ErrorMessages.ATTR_NAME_MUST_BE_STRING, keyObj); - } - - try { - return objectGetattrNode.execute(frame, self, key); - } catch (PException e) { - e.expectAttributeError(inliningTarget, errorProfile); - return objectGetattrNode.execute(frame, self.getFunction(), key); + @Cached StringNodes.CastToTruffleStringChecked1Node castToString, + @Cached GetClassNode.GetPythonObjectClassNode getClassNode, + @Cached LookupAttributeInMRONode.Dynamic lookup, + @Cached GetObjectSlotsNode getDescrSlotsNode, + @Cached CallSlotDescrGet callSlotDescrGet, + @Cached PyObjectGetAttr getAttrNode) { + TruffleString key = castToString.cast(inliningTarget, keyObj, ErrorMessages.ATTR_NAME_MUST_BE_STRING, keyObj); + Object type = getClassNode.execute(inliningTarget, self); + Object descr = lookup.execute(type, key); + if (!PGuards.isNoValue(descr)) { + TpSlots descrSlots = getDescrSlotsNode.execute(inliningTarget, descr); + if (descrSlots.tp_descr_get() != null) { + return callSlotDescrGet.execute(frame, inliningTarget, descrSlots.tp_descr_get(), descr, self, type); + } } + return getAttrNode.execute(frame, inliningTarget, self.function, key); } @Specialization(guards = "!isPMethod(self)") @@ -243,15 +240,4 @@ static Object kwDefaults(PMethod self, return (kwdefaults.length > 0) ? PFactory.createDict(PythonLanguage.get(inliningTarget), kwdefaults) : PNone.NONE; } } - - @Slot(SlotKind.tp_descr_get) - @GenerateUncached - @GenerateNodeFactory - public abstract static class GetNode extends DescrGetBuiltinNode { - @Specialization - @SuppressWarnings("unused") - static PMethod doGeneric(PMethod self, Object obj, Object cls) { - return self; - } - } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/PDecoratedMethod.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/PDecoratedMethod.java index c132d51694..acf047096e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/PDecoratedMethod.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/PDecoratedMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -43,11 +43,9 @@ import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.builtins.BoundBuiltinCallable; import com.oracle.graal.python.builtins.PythonBuiltinClassType; -import com.oracle.graal.python.builtins.objects.function.Signature; import com.oracle.graal.python.builtins.objects.object.PythonBuiltinObject; import com.oracle.graal.python.nodes.object.GetClassNode.GetPythonObjectClassNode; import com.oracle.graal.python.runtime.object.PFactory; -import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.object.Shape; /** @@ -87,14 +85,4 @@ public Object boundToObject(PythonBuiltinClassType binding, PythonLanguage langu } return this; } - - public String getName() { - CompilerDirectives.transferToInterpreterAndInvalidate(); - throw new UnsupportedOperationException(); - } - - public Signature getSignature() { - CompilerDirectives.transferToInterpreterAndInvalidate(); - throw new UnsupportedOperationException(); - } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java index a65a2ccd86..d10917d28f 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java @@ -260,6 +260,9 @@ public abstract class ErrorMessages { public static final TruffleString DESC_FOR_INDEX_S_FOR_S_DOESNT_APPLY_TO_P = tsLiteral("descriptor for index '%d' for %s doesn't apply to '%p' object"); public static final TruffleString DESC_S_FOR_N_DOESNT_APPLY_TO_N = tsLiteral("descriptor '%s' for '%N' objects doesn't apply to '%N' object"); public static final TruffleString GET_NONE_NONE_IS_INVALID = tsLiteral("__get__(None, None) is invalid"); + public static final TruffleString DESCRIPTOR_S_FOR_TYPE_S_NEEDS_EITHER_OBJ_OR_TYPE = tsLiteral("descriptor '%s' for type '%s' needs either an object or a type"); + public static final TruffleString DESCRIPTOR_S_FOR_TYPE_S_NEEDS_TYPE_NOT_P_AS_ARG_2 = tsLiteral("descriptor '%s' for type '%s' needs a type, not a '%p' as arg 2"); + public static final TruffleString DESCRIPTOR_S_REQUIRES_SUBTYPE_OF_S_BUT_RECEIVED_S = tsLiteral("descriptor '%s' requires a subtype of '%s' but received '%s'"); public static final TruffleString DESCRIPTOR_S_REQUIRES_S_OBJ_RECEIVED_P = tsLiteral("descriptor '%s' requires a '%s' object but received a '%p'"); public static final TruffleString DESCRIPTOR_REQUIRES_S_OBJ_RECEIVED_P = tsLiteral("descriptor requires a '%s' object but received a '%p'"); public static final TruffleString DESCRIPTOR_NEED_OBJ = tsLiteral("descriptor '%s' of '%s' object needs an argument"); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/classes/IsSubtypeNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/classes/IsSubtypeNode.java index 0232db2025..563e9a35fd 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/classes/IsSubtypeNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/classes/IsSubtypeNode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -165,6 +165,10 @@ protected static int sub(int a, int b) { return a - b; } + protected static MroSequenceStorage getMroUncached(Object cls) { + return GetMroStorageNode.executeUncached(cls); + } + @Specialization(guards = { "cachedCls != null", "getType(inliningTarget, cls, builtinTypeProfile, builtinClassProfile) == cachedCls", @@ -200,13 +204,12 @@ static boolean isVariableSubtypeOfConstantTypeCachedMultiContext(@SuppressWarnin @SuppressWarnings("unused") static boolean isSubtypeOfCached(Object derived, Object cls, @Bind Node inliningTarget, - @Cached("derived") Object cachedDerived, - @Cached("cls") Object cachedCls, + @Cached(value = "derived", weak = true) Object cachedDerived, + @Cached(value = "cls", weak = true) Object cachedCls, @Shared @Cached IsSameTypeNode isSameDerivedNode, @Shared @Cached IsSameTypeNode isSameClsNode, @Shared @Cached IsSameTypeNode isSameTypeInLoopNode, - @Shared @Cached GetMroStorageNode getMro, - @Cached("getMro.execute(inliningTarget, cachedDerived)") MroSequenceStorage mro, + @Cached(value = "getMroUncached(derived)", weak = true) MroSequenceStorage mro, @Cached("isInMro(inliningTarget, cachedCls, mro, mro.getInternalClassArray().length, isSameTypeInLoopNode)") boolean isInMro) { return isInMro; } @@ -225,9 +228,8 @@ static boolean isSubtypeOfCached(Object derived, Object cls, @InliningCutoff static boolean isSubtypeOfVariableTypeCached(@SuppressWarnings("unused") Object derived, Object cls, @Bind Node inliningTarget, - @Cached("derived") @SuppressWarnings("unused") Object cachedDerived, - @SuppressWarnings("unused") @Shared @Cached GetMroStorageNode getMro, - @Cached("getMro.execute(inliningTarget, cachedDerived)") MroSequenceStorage mro, + @Cached(value = "derived", weak = true) @SuppressWarnings("unused") Object cachedDerived, + @Cached(value = "getMroUncached(derived)", weak = true) MroSequenceStorage mro, @Cached("mro.getInternalClassArray().length") int sz, @Shared @Cached IsSameTypeNode isSameTypeInLoopNode, @Shared @Cached @SuppressWarnings("unused") IsSameTypeNode isSameDerivedNode) { @@ -252,9 +254,9 @@ static boolean isSubtypeOfVariableTypeCached(@SuppressWarnings("unused") Object @InliningCutoff static boolean isVariableSubtypeOfConstantTypeCached(@SuppressWarnings("unused") Object derived, @SuppressWarnings("unused") Object cls, @Bind Node inliningTarget, - @Cached("cls") @SuppressWarnings("unused") Object cachedCls, + @Cached(value = "cls", weak = true) @SuppressWarnings("unused") Object cachedCls, @SuppressWarnings("unused") @Shared @Cached GetMroStorageNode getMro, - @SuppressWarnings("unused") @Cached("getMro.execute(inliningTarget, cachedCls)") MroSequenceStorage baseMro, + @SuppressWarnings("unused") @Cached(value = "getMroUncached(cls)", weak = true) MroSequenceStorage baseMro, @Shared @Cached IsSameTypeNode isSameTypeInLoopNode, @Bind("getMro.execute(inliningTarget, derived).getInternalClassArray()") PythonAbstractClass[] mroAry, @SuppressWarnings("unused") @Cached("mroAry.length") int derivedMroLen,