Skip to content

Commit 1f8656d

Browse files
committed
[WIP] Refactoring: remove reversible builtins, clean up LookupAndCallXYZ nodes.
PullRequest: graalpython/3740
2 parents 3cd7cfd + d944124 commit 1f8656d

24 files changed

+319
-640
lines changed

graalpython/com.oracle.graal.python.test/src/tests/test_dict.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -1267,3 +1267,11 @@ def test_dict_values_eq():
12671267
d1 = {1: 1, 2: 2, 4: 4}
12681268
assert d1.values() != d1.values()
12691269

1270+
1271+
def test_missing_and_not_implemented():
1272+
class MyDict(dict):
1273+
def __missing__(self, key):
1274+
return NotImplemented
1275+
1276+
d = MyDict()
1277+
assert d['bogus_key'] == NotImplemented

graalpython/com.oracle.graal.python.test/src/tests/test_isinstance.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -271,4 +271,24 @@ def call_isinstance(expected, tpl):
271271

272272
called_instancecheck = 0
273273
assert isinstance(expected_other, tpl) is True
274-
assert called_instancecheck == 190
274+
assert called_instancecheck == 190
275+
276+
277+
def test_instancecheck_and_subclass_returns_not_iplemented():
278+
class MyMetaType(type):
279+
def __instancecheck__(cls, instance):
280+
return NotImplemented
281+
def __subclasscheck__(cls, instance):
282+
return NotImplemented
283+
284+
class MyMetaTypeInstance(metaclass=MyMetaType):
285+
pass
286+
287+
class UnrelatedClass():
288+
pass
289+
290+
o = UnrelatedClass
291+
# gives: DeprecationWarning: NotImplemented should not be used in a boolean context
292+
# but should still treat NotImplemented as True
293+
assert isinstance(o, MyMetaTypeInstance)
294+
assert issubclass(UnrelatedClass, MyMetaTypeInstance)

graalpython/com.oracle.graal.python.test/src/tests/test_string.py

+10-1
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-2017 Python Software Foundation
33
#
44
# Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -1195,3 +1195,12 @@ class S(str): pass
11951195
def test_literal_with_nonbmp_and_escapes():
11961196
# Check that escape processing didn't accidentally break the emoji into surrogates
11971197
assert len("\\🤗\\") == 3
1198+
1199+
1200+
def test_str_from_mmap():
1201+
import mmap
1202+
size = len("GraalPy")
1203+
with mmap.mmap(-1, size) as mm:
1204+
mm.write(b"GraalPy")
1205+
mm.seek(0)
1206+
assert str(mm, encoding='utf-8') == 'GraalPy'

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Builtin.java

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2017, 2025, Oracle and/or its affiliates.
33
* Copyright (c) 2013, Regents of the University of California
44
*
55
* All rights reserved.
@@ -97,15 +97,6 @@
9797
*/
9898
boolean declaresExplicitSelf() default false;
9999

100-
/**
101-
* Declares that this builtin needs to reverse the first and second argument it receives. This
102-
* implements the reverse operation wrappers from CPython. This only applies to binary and
103-
* ternary nodes.
104-
*
105-
* @see com.oracle.graal.python.nodes.function.BuiltinFunctionRootNode BuiltinFunctionRootNode
106-
*/
107-
boolean reverseOperation() default false;
108-
109100
String raiseErrorName() default StringLiterals.J_EMPTY_STRING;
110101

111102
boolean forceSplitDirectCalls() default false;

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/BuiltinConstructors.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import com.oracle.graal.python.builtins.objects.PNotImplemented;
113113
import com.oracle.graal.python.builtins.objects.buffer.PythonBufferAccessLibrary;
114114
import com.oracle.graal.python.builtins.objects.buffer.PythonBufferAcquireLibrary;
115+
import com.oracle.graal.python.builtins.objects.bytes.BytesCommonBuiltins;
115116
import com.oracle.graal.python.builtins.objects.bytes.BytesNodes;
116117
import com.oracle.graal.python.builtins.objects.bytes.PByteArray;
117118
import com.oracle.graal.python.builtins.objects.bytes.PBytes;
@@ -207,7 +208,6 @@
207208
import com.oracle.graal.python.nodes.builtins.TupleNodes;
208209
import com.oracle.graal.python.nodes.call.CallNode;
209210
import com.oracle.graal.python.nodes.call.special.CallUnaryMethodNode;
210-
import com.oracle.graal.python.nodes.call.special.LookupAndCallTernaryNode;
211211
import com.oracle.graal.python.nodes.call.special.LookupAndCallUnaryNode;
212212
import com.oracle.graal.python.nodes.call.special.LookupSpecialMethodSlotNode;
213213
import com.oracle.graal.python.nodes.classes.IsSubtypeNode;
@@ -1823,7 +1823,7 @@ static Object doBuffer(VirtualFrame frame, Object cls, Object obj, Object encodi
18231823
@Exclusive @Cached InlinedConditionProfile isPStringProfile,
18241824
@Exclusive @CachedLibrary("obj") PythonBufferAcquireLibrary acquireLib,
18251825
@Exclusive @CachedLibrary(limit = "1") PythonBufferAccessLibrary bufferLib,
1826-
@Exclusive @Cached("create(T_DECODE)") LookupAndCallTernaryNode callDecodeNode,
1826+
@Exclusive @Cached BytesCommonBuiltins.DecodeNode decodeNode,
18271827
@Shared @Cached TypeNodes.GetInstanceShape getInstanceShape,
18281828
@Exclusive @Cached PRaiseNode raiseNode) {
18291829
Object buffer;
@@ -1837,7 +1837,7 @@ static Object doBuffer(VirtualFrame frame, Object cls, Object obj, Object encodi
18371837
// TODO don't copy, CPython creates a memoryview
18381838
PBytes bytesObj = PFactory.createBytes(PythonLanguage.get(inliningTarget), bufferLib.getCopiedByteArray(buffer));
18391839
Object en = encoding == PNone.NO_VALUE ? T_UTF8 : encoding;
1840-
Object result = assertNoJavaString(callDecodeNode.execute(frame, bytesObj, en, errors));
1840+
Object result = assertNoJavaString(decodeNode.execute(frame, bytesObj, en, errors));
18411841
if (isStringProfile.profile(inliningTarget, result instanceof TruffleString)) {
18421842
return asPString(cls, (TruffleString) result, inliningTarget, isPrimitiveProfile, getInstanceShape);
18431843
} else if (isPStringProfile.profile(inliningTarget, result instanceof PString)) {
@@ -1882,7 +1882,7 @@ static Object doNativeSubclassEncodeErr(VirtualFrame frame, Object cls, Object o
18821882
@Exclusive @Cached InlinedConditionProfile isPStringProfile,
18831883
@Exclusive @CachedLibrary("obj") PythonBufferAcquireLibrary acquireLib,
18841884
@Exclusive @CachedLibrary(limit = "1") PythonBufferAccessLibrary bufferLib,
1885-
@Exclusive @Cached("create(T_DECODE)") LookupAndCallTernaryNode callDecodeNode,
1885+
@Exclusive @Cached BytesCommonBuiltins.DecodeNode decodeNode,
18861886
@Shared @Cached(neverDefault = true) CExtNodes.StringSubtypeNew subtypeNew,
18871887
@Shared @Cached TypeNodes.GetInstanceShape getInstanceShape,
18881888
@Exclusive @Cached PRaiseNode raiseNode) {
@@ -1895,7 +1895,7 @@ static Object doNativeSubclassEncodeErr(VirtualFrame frame, Object cls, Object o
18951895
try {
18961896
PBytes bytesObj = PFactory.createBytes(PythonLanguage.get(inliningTarget), bufferLib.getCopiedByteArray(buffer));
18971897
Object en = encoding == PNone.NO_VALUE ? T_UTF8 : encoding;
1898-
Object result = assertNoJavaString(callDecodeNode.execute(frame, bytesObj, en, errors));
1898+
Object result = assertNoJavaString(decodeNode.execute(frame, bytesObj, en, errors));
18991899
if (isStringProfile.profile(inliningTarget, result instanceof TruffleString)) {
19001900
return subtypeNew.call(cls, asPString(cls, (TruffleString) result, inliningTarget, isPrimitiveProfile, getInstanceShape));
19011901
} else if (isPStringProfile.profile(inliningTarget, result instanceof PString)) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/BuiltinFunctions.java

+20-17
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import static com.oracle.graal.python.builtins.modules.io.IONodes.T_WRITE;
3434
import static com.oracle.graal.python.builtins.objects.PNone.NONE;
3535
import static com.oracle.graal.python.builtins.objects.PNone.NO_VALUE;
36-
import static com.oracle.graal.python.builtins.objects.PNotImplemented.NOT_IMPLEMENTED;
3736
import static com.oracle.graal.python.compiler.RaisePythonExceptionErrorCallback.raiseSyntaxError;
3837
import static com.oracle.graal.python.nodes.BuiltinNames.J_ABS;
3938
import static com.oracle.graal.python.nodes.BuiltinNames.J_ALL;
@@ -166,7 +165,6 @@
166165
import com.oracle.graal.python.builtins.objects.type.TypeNodes.IsSameTypeNode;
167166
import com.oracle.graal.python.builtins.objects.type.TypeNodes.IsTypeNode;
168167
import com.oracle.graal.python.builtins.objects.type.slots.TpSlotIterNext.CallSlotTpIterNextNode;
169-
import com.oracle.graal.python.lib.RichCmpOp;
170168
import com.oracle.graal.python.compiler.Compiler;
171169
import com.oracle.graal.python.compiler.RaisePythonExceptionErrorCallback;
172170
import com.oracle.graal.python.lib.IteratorExhausted;
@@ -200,6 +198,7 @@
200198
import com.oracle.graal.python.lib.PyObjectStrAsObjectNode;
201199
import com.oracle.graal.python.lib.PyObjectStrAsTruffleStringNode;
202200
import com.oracle.graal.python.lib.PyUnicodeFSDecoderNode;
201+
import com.oracle.graal.python.lib.RichCmpOp;
203202
import com.oracle.graal.python.nodes.BuiltinNames;
204203
import com.oracle.graal.python.nodes.ErrorMessages;
205204
import com.oracle.graal.python.nodes.PConstructAndRaiseNode;
@@ -224,6 +223,7 @@
224223
import com.oracle.graal.python.nodes.call.special.LookupAndCallBinaryNode;
225224
import com.oracle.graal.python.nodes.call.special.LookupAndCallUnaryNode;
226225
import com.oracle.graal.python.nodes.call.special.LookupSpecialMethodSlotNode;
226+
import com.oracle.graal.python.nodes.call.special.SpecialMethodNotFound;
227227
import com.oracle.graal.python.nodes.classes.IsSubtypeNode;
228228
import com.oracle.graal.python.nodes.frame.GetFrameLocalsNode;
229229
import com.oracle.graal.python.nodes.frame.ReadCallerFrameNode;
@@ -1450,11 +1450,12 @@ public IsInstanceNode createRecursive(byte newDepth) {
14501450

14511451
private static TriState isInstanceCheckInternal(VirtualFrame frame, Object instance, Object cls, LookupAndCallBinaryNode instanceCheckNode,
14521452
PyObjectIsTrueNode castToBooleanNode) {
1453-
Object instanceCheckResult = instanceCheckNode.executeObject(frame, cls, instance);
1454-
if (instanceCheckResult == NOT_IMPLEMENTED) {
1453+
try {
1454+
Object instanceCheckResult = instanceCheckNode.executeObject(frame, cls, instance);
1455+
return TriState.valueOf(castToBooleanNode.execute(frame, instanceCheckResult));
1456+
} catch (SpecialMethodNotFound ignore) {
14551457
return TriState.UNDEFINED;
14561458
}
1457-
return TriState.valueOf(castToBooleanNode.execute(frame, instanceCheckResult));
14581459
}
14591460

14601461
@Specialization(guards = "isPythonClass(cls)")
@@ -1508,11 +1509,12 @@ static boolean isSubclass(VirtualFrame frame, Object derived, Object cls,
15081509
@Cached("create(Subclasscheck)") LookupAndCallBinaryNode subclassCheckNode,
15091510
@Cached PyObjectIsTrueNode castToBooleanNode,
15101511
@Cached IsSubtypeNode isSubtypeNode) {
1511-
Object instanceCheckResult = subclassCheckNode.executeObject(frame, cls, derived);
1512-
if (instanceCheckResult != NOT_IMPLEMENTED) {
1512+
try {
1513+
Object instanceCheckResult = subclassCheckNode.executeObject(frame, cls, derived);
15131514
return castToBooleanNode.execute(frame, instanceCheckResult);
1515+
} catch (SpecialMethodNotFound ignore) {
1516+
return isSubtypeNode.execute(frame, derived, cls);
15141517
}
1515-
return isSubtypeNode.execute(frame, derived, cls);
15161518
}
15171519

15181520
@NeverDefault
@@ -1930,14 +1932,15 @@ public static Object format(VirtualFrame frame, Object obj, Object formatSpec,
19301932
@Cached InlinedConditionProfile formatIsNoValueProfile,
19311933
@Cached PRaiseNode raiseNode) {
19321934
Object format = formatIsNoValueProfile.profile(inliningTarget, isNoValue(formatSpec)) ? T_EMPTY_STRING : formatSpec;
1933-
Object res = callFormat.executeObject(frame, obj, format);
1934-
if (res == NO_VALUE) {
1935+
try {
1936+
Object res = callFormat.executeObject(frame, obj, format);
1937+
if (!PGuards.isString(res)) {
1938+
throw raiseNode.raise(inliningTarget, TypeError, ErrorMessages.S_MUST_RETURN_S_NOT_P, T___FORMAT__, "str", res);
1939+
}
1940+
return res;
1941+
} catch (SpecialMethodNotFound ignore) {
19351942
throw raiseNode.raise(inliningTarget, TypeError, ErrorMessages.TYPE_DOESNT_DEFINE_FORMAT, obj);
19361943
}
1937-
if (!PGuards.isString(res)) {
1938-
throw raiseNode.raise(inliningTarget, TypeError, ErrorMessages.S_MUST_RETURN_S_NOT_P, T___FORMAT__, "str", res);
1939-
}
1940-
return res;
19411944
}
19421945

19431946
@NeverDefault
@@ -1980,11 +1983,11 @@ static Object round(VirtualFrame frame, Object x, Object n,
19801983
@Bind("this") Node inliningTarget,
19811984
@Cached("create(Round)") LookupAndCallBinaryNode callRound,
19821985
@Shared @Cached PRaiseNode raiseNode) {
1983-
Object result = callRound.executeObject(frame, x, n);
1984-
if (result == NOT_IMPLEMENTED) {
1986+
try {
1987+
return callRound.executeObject(frame, x, n);
1988+
} catch (SpecialMethodNotFound ignore) {
19851989
throw raiseNode.raise(inliningTarget, TypeError, ErrorMessages.TYPE_DOESNT_DEFINE_METHOD, x, T___ROUND__);
19861990
}
1987-
return result;
19881991
}
19891992
}
19901993

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/MarshalModuleBuiltins.java

+4-7
Original file line numberDiff line numberDiff line change
@@ -151,30 +151,27 @@ protected ArgumentClinicProvider getArgumentClinic() {
151151
return DumpNodeClinicProviderGen.INSTANCE;
152152
}
153153

154-
@NeverDefault
155-
protected static LookupAndCallBinaryNode createCallWriteNode() {
156-
return LookupAndCallBinaryNode.create(T_WRITE);
157-
}
158-
159154
@Specialization
160155
static Object doit(VirtualFrame frame, Object value, Object file, int version,
161156
@Bind("this") Node inliningTarget,
162157
@Bind PythonContext context,
163158
@Cached("createFor(this)") IndirectCallData indirectCallData,
164-
@Cached("createCallWriteNode()") LookupAndCallBinaryNode callNode,
159+
@Cached PyObjectCallMethodObjArgs callMethod,
165160
@Cached PRaiseNode raiseNode) {
166161
PythonLanguage language = context.getLanguage(inliningTarget);
167162
PythonContext.PythonThreadState threadState = context.getThreadState(language);
168163
Object savedState = IndirectCallContext.enter(frame, threadState, indirectCallData);
164+
byte[] data;
169165
try {
170-
return callNode.executeObject(frame, file, PFactory.createBytes(language, Marshal.dump(context, value, version)));
166+
data = Marshal.dump(context, value, version);
171167
} catch (IOException e) {
172168
throw CompilerDirectives.shouldNotReachHere(e);
173169
} catch (Marshal.MarshalError me) {
174170
throw raiseNode.raise(inliningTarget, me.type, me.message, me.arguments);
175171
} finally {
176172
IndirectCallContext.exit(frame, threadState, savedState);
177173
}
174+
return callMethod.execute(frame, inliningTarget, file, T_WRITE, PFactory.createBytes(language, data));
178175
}
179176
}
180177

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/RandomModuleBuiltins.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@
3737
import com.oracle.graal.python.builtins.objects.PNone;
3838
import com.oracle.graal.python.builtins.objects.random.PRandom;
3939
import com.oracle.graal.python.builtins.objects.type.TypeNodes;
40+
import com.oracle.graal.python.nodes.PRaiseNode;
4041
import com.oracle.graal.python.nodes.call.special.LookupAndCallBinaryNode;
42+
import com.oracle.graal.python.nodes.call.special.SpecialMethodNotFound;
4143
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
4244
import com.oracle.graal.python.nodes.function.PythonBuiltinNode;
4345
import com.oracle.graal.python.runtime.object.PFactory;
46+
import com.oracle.truffle.api.CompilerDirectives;
4447
import com.oracle.truffle.api.dsl.Bind;
4548
import com.oracle.truffle.api.dsl.Cached;
4649
import com.oracle.truffle.api.dsl.GenerateNodeFactory;
@@ -70,7 +73,12 @@ PRandom random(VirtualFrame frame, Object cls, Object seed,
7073
@Bind PythonLanguage language,
7174
@Cached TypeNodes.GetInstanceShape getInstanceShape) {
7275
PRandom random = PFactory.createRandom(language, cls, getInstanceShape.execute(cls));
73-
setSeed.executeObject(frame, random, seed != PNone.NO_VALUE ? seed : PNone.NONE);
76+
try {
77+
setSeed.executeObject(frame, random, seed != PNone.NO_VALUE ? seed : PNone.NONE);
78+
} catch (SpecialMethodNotFound ignore) {
79+
CompilerDirectives.transferToInterpreterAndInvalidate();
80+
throw PRaiseNode.raiseStatic(this, PythonBuiltinClassType.SystemError);
81+
}
7482
return random;
7583
}
7684
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/dict/DictBuiltins.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,20 @@
7272
import com.oracle.graal.python.builtins.objects.type.slots.TpSlotLen.LenBuiltinNode;
7373
import com.oracle.graal.python.builtins.objects.type.slots.TpSlotMpAssSubscript.MpAssSubscriptBuiltinNode;
7474
import com.oracle.graal.python.builtins.objects.type.slots.TpSlotRichCompare.RichCmpBuiltinNode;
75-
import com.oracle.graal.python.lib.RichCmpOp;
7675
import com.oracle.graal.python.builtins.objects.type.slots.TpSlotSqContains.SqContainsBuiltinNode;
7776
import com.oracle.graal.python.lib.IteratorExhausted;
7877
import com.oracle.graal.python.lib.PyDictCheckNode;
7978
import com.oracle.graal.python.lib.PyDictSetDefault;
8079
import com.oracle.graal.python.lib.PyIterNextNode;
8180
import com.oracle.graal.python.lib.PyObjectGetIter;
8281
import com.oracle.graal.python.lib.PyObjectSetItem;
82+
import com.oracle.graal.python.lib.RichCmpOp;
8383
import com.oracle.graal.python.nodes.ErrorMessages;
8484
import com.oracle.graal.python.nodes.PGuards;
8585
import com.oracle.graal.python.nodes.PRaiseNode;
8686
import com.oracle.graal.python.nodes.call.CallNode;
8787
import com.oracle.graal.python.nodes.call.special.LookupAndCallBinaryNode;
88+
import com.oracle.graal.python.nodes.call.special.SpecialMethodNotFound;
8889
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
8990
import com.oracle.graal.python.nodes.function.PythonBuiltinNode;
9091
import com.oracle.graal.python.nodes.function.builtins.PythonBinaryBuiltinNode;
@@ -341,11 +342,11 @@ static Object missing(VirtualFrame frame, Object self, Object key,
341342
@Bind("this") Node inliningTarget,
342343
@Cached("create(Missing)") LookupAndCallBinaryNode callMissing,
343344
@Cached PRaiseNode raiseNode) {
344-
Object result = callMissing.executeObject(frame, self, key);
345-
if (result == PNotImplemented.NOT_IMPLEMENTED) {
345+
try {
346+
return callMissing.executeObject(frame, self, key);
347+
} catch (SpecialMethodNotFound ignored) {
346348
throw raiseNode.raise(inliningTarget, KeyError, new Object[]{key});
347349
}
348-
return result;
349350
}
350351
}
351352

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/foreign/ForeignNumberBuiltins.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import com.oracle.graal.python.nodes.SpecialMethodNames;
8282
import com.oracle.graal.python.nodes.call.special.LookupAndCallBinaryNode;
8383
import com.oracle.graal.python.nodes.call.special.LookupAndCallUnaryNode;
84+
import com.oracle.graal.python.nodes.call.special.SpecialMethodNotFound;
8485
import com.oracle.graal.python.nodes.expression.BinaryOpNode;
8586
import com.oracle.graal.python.nodes.expression.UnaryOpNode;
8687
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
@@ -609,7 +610,11 @@ Object doGeneric(VirtualFrame frame, Object self, Object n,
609610
@Cached UnboxNode unboxNode,
610611
@Cached("create(Round)") LookupAndCallBinaryNode callRound) {
611612
Object unboxed = unboxNode.execute(inliningTarget, self);
612-
return callRound.executeObject(frame, unboxed, n);
613+
try {
614+
return callRound.executeObject(frame, unboxed, n);
615+
} catch (SpecialMethodNotFound ignore) {
616+
throw CompilerDirectives.shouldNotReachHere();
617+
}
613618
}
614619
}
615620

0 commit comments

Comments
 (0)