Skip to content

Commit 6c7551e

Browse files
committed
[GR-55196] Implement tp_hash and tp_richcompare.
PullRequest: graalpython/3628
2 parents b3bdf29 + 420c934 commit 6c7551e

File tree

115 files changed

+3134
-4785
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+3134
-4785
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.annotations;
42+
43+
import java.lang.annotation.ElementType;
44+
import java.lang.annotation.Retention;
45+
import java.lang.annotation.RetentionPolicy;
46+
import java.lang.annotation.Target;
47+
48+
/**
49+
* Indicates that the annotated class inheriting {@code PythonBuiltins} should have slot
50+
* {@code tp_hash} filled with the {@code PyObject_HashNotImplemented} placeholder. There is some
51+
* special inheritance semantics for this specific slot value.
52+
*/
53+
@Retention(RetentionPolicy.RUNTIME)
54+
@Target(ElementType.TYPE)
55+
public @interface HashNotImplemented {
56+
}

graalpython/com.oracle.graal.python.annotations/src/com/oracle/graal/python/annotations/Slot.java

+6
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,16 @@ enum SlotKind {
185185
mp_subscript("__getitem__"),
186186
/** o[key] = value */
187187
mp_ass_subscript("__setitem__"),
188+
/** comparison operations: >,=>, ==, !=, <, <= */
189+
tp_richcompare("__lt__, __le__, __eq__, __ne__, __gt__, __ge__"),
188190
/** type descriptor get */
189191
tp_descr_get("__get__"),
190192
/** type descriptor set/delete */
191193
tp_descr_set("__set__, __delete__"),
194+
/**
195+
* hash code. See also if {@link HashNotImplemented} is not more appropriate.
196+
*/
197+
tp_hash("__hash__"),
192198
/** get object attribute */
193199
tp_getattro("__getattribute__, __getattr__"),
194200
/** set/delete object attribute */

graalpython/com.oracle.graal.python.cext/src/typeobject.c

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright (c) 2018, 2024, Oracle and/or its affiliates.
1+
/* Copyright (c) 2018, 2025, Oracle and/or its affiliates.
22
* Copyright (C) 1996-2022 Python Software Foundation
33
*
44
* Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -9362,13 +9362,8 @@ static int type_ready_graalpy_slot_conv(PyTypeObject* cls) {
93629362
ADD_SLOT_CONV("__delattr__", cls->tp_setattro, -3, JWRAPPER_DELATTRO);
93639363
ADD_SLOT_CONV("__clear__", cls->tp_clear, -1, JWRAPPER_INQUIRY);
93649364

9365-
/* IMPORTANT NOTE: If the class already provides 'tp_richcompare' but this is the default
9366-
'object.__truffle_richcompare__' function, then we need to break a recursive cycle since
9367-
the default function dispatches to the individual comparison functions which would in
9368-
this case again invoke 'object.__truffle_richcompare__'. */
93699365
richcmpfunc richcompare = cls->tp_richcompare;
9370-
if (richcompare && richcompare != PyBaseObject_Type.tp_richcompare) {
9371-
ADD_SLOT_CONV("__compare__", richcompare, -3, JWRAPPER_RICHCMP);
9366+
if (richcompare) {
93729367
ADD_SLOT_CONV("__lt__", richcompare, -2, JWRAPPER_LT);
93739368
ADD_SLOT_CONV("__le__", richcompare, -2, JWRAPPER_LE);
93749369
ADD_SLOT_CONV("__eq__", richcompare, -2, JWRAPPER_EQ);

graalpython/com.oracle.graal.python.processor/src/com/oracle/graal/python/processor/ConverterFactory.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -210,7 +210,8 @@ public static ConverterFactory[] getForClass(TypeElement conversionClass) throws
210210
private static ConverterFactory[] forBuiltin(Elements elementUtils, String className) throws ProcessingError {
211211
TypeElement type = elementUtils.getTypeElement(CLINIC_PACKAGE + "." + className);
212212
if (type == null) {
213-
throw new ProcessingError(null, "Unable to find built-in argument clinic conversion node " + CLINIC_PACKAGE + "." + className);
213+
throw new ProcessingError(null, "Unable to find built-in argument clinic conversion node " + CLINIC_PACKAGE + "." + className +
214+
". This may be also a sign that Truffle DSL annotation processor failed to process this class.");
214215
}
215216
return getForClass(type);
216217
}

graalpython/com.oracle.graal.python.processor/src/com/oracle/graal/python/processor/SlotsMapping.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,18 @@ static String getSlotBaseClass(Slot s) {
7373
case mp_subscript -> "TpSlotBinaryFunc.TpSlotMpSubscript";
7474
case mp_ass_subscript -> "TpSlotMpAssSubscript.TpSlotMpAssSubscriptBuiltin";
7575
case tp_getattro -> "TpSlotGetAttr.TpSlotGetAttrBuiltin";
76+
case tp_richcompare -> "TpSlotRichCompare.TpSlotRichCmpBuiltin" + getSuffix(s.isComplex());
7677
case tp_descr_get -> "TpSlotDescrGet.TpSlotDescrGetBuiltin" + getSuffix(s.isComplex());
7778
case tp_descr_set -> "TpSlotDescrSet.TpSlotDescrSetBuiltin";
7879
case tp_setattro -> "TpSlotSetAttr.TpSlotSetAttrBuiltin";
7980
case tp_iternext -> "TpSlotIterNext.TpSlotIterNextBuiltin";
81+
case tp_hash -> "TpSlotHashFun.TpSlotHashBuiltin";
8082
};
8183
}
8284

8385
static String getSlotNodeBaseClass(Slot s) {
8486
return switch (s.value()) {
87+
case tp_richcompare -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotRichCompare.RichCmpBuiltinNode";
8588
case tp_descr_get -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.DescrGetBuiltinNode";
8689
case nb_bool -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotInquiry.NbBoolBuiltinNode";
8790
case nb_index, nb_int, nb_float, nb_absolute, nb_positive, nb_negative, nb_invert,
@@ -107,12 +110,14 @@ static String getSlotNodeBaseClass(Slot s) {
107110
case tp_getattro -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotGetAttr.GetAttrBuiltinNode";
108111
case tp_descr_set -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrSet.DescrSetBuiltinNode";
109112
case tp_setattro -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotSetAttr.SetAttrBuiltinNode";
113+
case tp_hash -> "com.oracle.graal.python.builtins.objects.type.slots.TpSlotHashFun.HashBuiltinNode";
110114
};
111115
}
112116

113117
static String getUncachedExecuteSignature(SlotKind s) {
114118
return switch (s) {
115119
case nb_bool -> "boolean executeUncached(Object self)";
120+
case tp_richcompare -> "Object executeUncached(Object self, Object obj, com.oracle.graal.python.lib.RichCmpOp op)";
116121
case tp_descr_get -> "Object executeUncached(Object self, Object obj, Object type)";
117122
case sq_length, mp_length -> "int executeUncached(Object self)";
118123
default -> throw new AssertionError("Should not reach here: should be always complex");
@@ -128,14 +133,15 @@ static boolean supportsComplex(SlotKind s) {
128133

129134
static boolean supportsSimple(SlotKind s) {
130135
return switch (s) {
131-
case nb_bool, sq_length, mp_length, tp_descr_get -> true;
136+
case nb_bool, sq_length, mp_length, tp_descr_get, tp_richcompare -> true;
132137
default -> false;
133138
};
134139
}
135140

136141
static String getUncachedExecuteCall(SlotKind s) {
137142
return switch (s) {
138143
case nb_bool -> "executeBool(null, self)";
144+
case tp_richcompare -> "execute(null, self, obj, op)";
139145
case sq_length, mp_length -> "executeInt(null, self)";
140146
case tp_descr_get -> "execute(null, self, obj, type)";
141147
default -> throw new AssertionError("Should not reach here: should be always complex");

graalpython/com.oracle.graal.python.processor/src/com/oracle/graal/python/processor/SlotsProcessor.java

+50-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -63,6 +63,7 @@
6363
import javax.tools.Diagnostic.Kind;
6464
import javax.tools.JavaFileObject;
6565

66+
import com.oracle.graal.python.annotations.HashNotImplemented;
6667
import com.oracle.graal.python.annotations.Slot;
6768
import com.oracle.graal.python.annotations.Slot.SlotKind;
6869
import com.oracle.graal.python.annotations.Slot.Slots;
@@ -72,11 +73,27 @@ public class SlotsProcessor extends AbstractProcessor {
7273
private static final boolean LOGGING = false;
7374

7475
public record TpSlotData(Slot slot, TypeElement enclosingType, TypeElement slotNodeType) {
76+
public boolean isHashNotImplemented() {
77+
return slotNodeType == null;
78+
}
79+
80+
public static TpSlotData createHashNotImplemented(TypeElement enclosingType) {
81+
return new TpSlotData(null, enclosingType, null);
82+
}
83+
84+
public String builderCall() {
85+
if (isHashNotImplemented()) {
86+
return ".set(TpSlots.TpSlotMeta.TP_HASH, com.oracle.graal.python.builtins.objects.type.slots.TpSlotHashFun.HASH_NOT_IMPLEMENTED)";
87+
}
88+
return String.format(".set(TpSlots.TpSlotMeta.%s, %s.INSTANCE)", //
89+
slot.value().name().toUpperCase(Locale.ROOT), //
90+
getSlotImplName(slot.value()));
91+
}
7592
}
7693

7794
@Override
7895
public Set<String> getSupportedAnnotationTypes() {
79-
return Set.of(Slot.class.getName());
96+
return Set.of(Slot.class.getName(), HashNotImplemented.class.getName());
8097
}
8198

8299
@Override
@@ -109,7 +126,14 @@ private void doProcess(RoundEnvironment roundEnv) throws IOException, Processing
109126
private void validate(HashMap<TypeElement, Set<TpSlotData>> enclosingTypes) throws ProcessingError {
110127
var typeCache = new TypeCache(processingEnv);
111128
for (Entry<TypeElement, Set<TpSlotData>> enclosingType : enclosingTypes.entrySet()) {
129+
boolean seenHashNotImplemented = false;
130+
boolean seenHash = false;
112131
for (TpSlotData slot : enclosingType.getValue()) {
132+
if (slot.isHashNotImplemented()) {
133+
seenHashNotImplemented = true;
134+
continue;
135+
}
136+
seenHash |= slot.slot.value() == SlotKind.tp_hash;
113137
if (slot.slot.isComplex()) {
114138
if (!SlotsMapping.supportsComplex(slot.slot().value())) {
115139
throw error(slot.slotNodeType, "Slot does not support complex builtins. The support can be added.");
@@ -125,14 +149,22 @@ private void validate(HashMap<TypeElement, Set<TpSlotData>> enclosingTypes) thro
125149
throw error(slot.slotNodeType, "Slot does not inherit from expected base class '%s'", baseName);
126150
}
127151
}
152+
if (seenHash && seenHashNotImplemented) {
153+
throw error(enclosingType.getKey(), "Annotation %s cannot be use when there is also %s(tp_hash). Remove one or the other.",
154+
HashNotImplemented.class.getSimpleName(),
155+
Slot.class.getSimpleName());
156+
}
128157
}
129158
}
130159

131160
@SuppressWarnings("try")
132161
private void writeCode(HashMap<TypeElement, Set<TpSlotData>> enclosingTypes) throws IOException {
133-
for (Entry<TypeElement, Set<TpSlotData>> enclosingType : enclosingTypes.entrySet()) {
134-
String pkgName = getPackage(enclosingType.getKey());
135-
String className = enclosingType.getKey().getSimpleName() + "SlotsGen";
162+
for (Entry<TypeElement, Set<TpSlotData>> enclosingTypeAndSlots : enclosingTypes.entrySet()) {
163+
Set<TpSlotData> slots = enclosingTypeAndSlots.getValue();
164+
TypeElement enclosingType = enclosingTypeAndSlots.getKey();
165+
166+
String pkgName = getPackage(enclosingType);
167+
String className = enclosingType.getSimpleName() + "SlotsGen";
136168
String sourceFile = pkgName + "." + className;
137169
log("Generating file '%s'", sourceFile);
138170

@@ -146,10 +178,10 @@ private void writeCode(HashMap<TypeElement, Set<TpSlotData>> enclosingTypes) thr
146178
w.writeLn();
147179
w.writeLn("public class %s {", className);
148180
try (Block i = w.newIndent()) {
149-
for (TpSlotData slot : enclosingType.getValue()) {
181+
for (TpSlotData slot : slots) {
150182
writeSlot(w, slot);
151183
}
152-
writeSlotsStaticField(w, enclosingType.getValue());
184+
writeSlotsStaticField(w, enclosingType, slots);
153185
}
154186
w.writeLn("}");
155187
}
@@ -168,6 +200,9 @@ private void writeImports(CodeWriter w) throws IOException {
168200

169201
@SuppressWarnings("try")
170202
private void writeSlot(CodeWriter w, TpSlotData slot) throws IOException {
203+
if (slot.isHashNotImplemented()) {
204+
return;
205+
}
171206
log("Writing slot node %s", slot.slotNodeType);
172207
String slotImplName = getSlotImplName(slot.slot.value());
173208
String genericArg = "";
@@ -202,13 +237,11 @@ private void writeSlot(CodeWriter w, TpSlotData slot) throws IOException {
202237
}
203238

204239
@SuppressWarnings("try")
205-
private static void writeSlotsStaticField(CodeWriter w, Set<TpSlotData> slots) throws IOException {
240+
private static void writeSlotsStaticField(CodeWriter w, TypeElement enclosingType, Set<TpSlotData> slots) throws IOException {
206241
w.writeLn("static final TpSlots SLOTS = TpSlots.newBuilder()");
207242
try (Block i3 = w.newIndent()) {
208243
String defs = slots.stream().//
209-
map(s -> String.format(".set(TpSlots.TpSlotMeta.%s, %s.INSTANCE)", //
210-
s.slot.value().name().toUpperCase(Locale.ROOT), //
211-
getSlotImplName(s.slot.value()))).//
244+
map(TpSlotData::builderCall).//
212245
collect(Collectors.joining("\n"));
213246
w.writeLn("%s.", defs);
214247
w.writeLn("build();");
@@ -237,6 +270,12 @@ private HashMap<TypeElement, Set<TpSlotData>> collectEnclosingTypes(RoundEnviron
237270
tpSlotDataSet.add(new TpSlotData(slotAnnotation, enclosingType, type));
238271
}
239272
}
273+
elements = new HashSet<>(roundEnv.getElementsAnnotatedWithAny(Set.of(HashNotImplemented.class)));
274+
for (Element element : elements) {
275+
TypeElement typeElement = (TypeElement) element;
276+
enclosingTypes.computeIfAbsent(typeElement, key -> new HashSet<>()).//
277+
add(TpSlotData.createHashNotImplemented(typeElement));
278+
}
240279
return enclosingTypes;
241280
}
242281

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/objects/ObjectHashMapTests.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -52,6 +52,7 @@
5252
import java.util.Random;
5353
import java.util.stream.Collectors;
5454

55+
import com.oracle.graal.python.lib.RichCmpOp;
5556
import org.junit.Assert;
5657
import org.junit.Test;
5758

@@ -69,7 +70,6 @@
6970
import com.oracle.graal.python.builtins.objects.common.ObjectHashMap.PopNode;
7071
import com.oracle.graal.python.lib.PyObjectHashNode;
7172
import com.oracle.graal.python.lib.PyObjectRichCompareBool;
72-
import com.oracle.graal.python.lib.PyObjectRichCompareBool.Comparison;
7373
import com.oracle.truffle.api.frame.Frame;
7474
import com.oracle.truffle.api.interop.TruffleObject;
7575
import com.oracle.truffle.api.nodes.Node;
@@ -86,9 +86,9 @@ public static final class DictKey implements TruffleObject {
8686
}
8787
}
8888

89-
private static final class EqNodeStub extends PyObjectRichCompareBool.EqNode {
89+
private static final class EqNodeStub extends PyObjectRichCompareBool {
9090
@Override
91-
protected boolean execute(Frame frame, Node inliningTarget, Object a, Object b, Comparison cmp) {
91+
public boolean execute(Frame frame, Node inliningTarget, Object a, Object b, RichCmpOp cmp) {
9292
// Sanity check: we do not use any other keys in the tests
9393
assert a instanceof Long || a instanceof DictKey;
9494
assert b instanceof Long || b instanceof DictKey;

0 commit comments

Comments
 (0)