Repository navigation
Description
Describe the bug
Found by differential testing against CPython 3.12. All 15 items reproduce on GraalPy 25.4.4.1.1, and CPython 3.12.13 and 3.13.16 give the same expected output for each. Every item under "Steps to reproduce" has a reproduction, both outputs and the cause, with a suggested fix folded underneath.
| # | API | GraalPy | CPython |
|---|---|---|---|
| 1 | callable(classmethod(f)) |
True |
False |
| 2 | type('X', (3,), {}) |
UnsupportedSpecializationException, exit 1 |
TypeError |
| 3 | vars(type('C', (), {})) key order |
__module__ third |
__module__ first |
| 4 | NotImplementedType.__bool__ |
missing | present, warns |
| 5 | frozenset(F()) with a subclass instance |
F |
frozenset |
| 6 | repr(5e-324) |
4.9e-324 |
5e-324 |
| 7 | compile(..., optimize=5) |
TypeError |
ValueError |
| 8 | pow(1, int, 1j) |
ValueError |
TypeError |
| 9 | list subclass + list |
subclass | list |
| 10 | bytearray item assignment out of range |
IndexError |
TypeError, ValueError |
| 11 | math.gcd / math.lcm with one __index__ argument |
TypeError |
int |
| 12 | itertools.combinations(1, -1), combinations_with_replacement(1, -1) |
ValueError |
TypeError |
| 13 | itertools.count(True) |
count(True) |
count(1) |
| 14 | functools.lru_cache(typed='x') |
TypeError |
accepted |
| 15 | bisect.bisect_right / bisect_left with hi=-1 |
0 |
len(a) used |
Operating system
Linux
CPU architecture
ARM64
GraalPy version
25.4.4.1.1 (Python 3.13.14); code read on master c1c5cef
JDK version
No response
Context configuration
No response
Steps to reproduce
1. classmethod objects are callable
cm = classmethod(lambda: 1)| Expression | GraalPy | CPython |
|---|---|---|
callable(cm) |
True |
False |
iter(cm, None) |
<callable_iterator object at 0x...> |
TypeError: iter(v, w): v must be callable |
cm() |
1 |
TypeError: 'classmethod' object is not callable |
ClassmethodCommonBuiltins.CallNode registers tp_call in the slots that classmethod shares with classmethod_descriptor (ClassmethodCommonBuiltins.java#L126-L135). In CPython PyClassMethod_Type has no tp_call (funcobject.c#L1139); only classmethod_descriptor is callable.
Fix suggestion
Move CallNode from ClassmethodCommonBuiltins to BuiltinClassmethodBuiltins, so only classmethod_descriptor gets tp_call.
2. type() with a non-type base crashes when the error message is read
try:
type('X', (3,), {})
except TypeError as e:
print('TypeError:', e)GraalPy:
com.oracle.truffle.api.dsl.UnsupportedSpecializationException: Unexpected values provided for TypeNodesFactory.GetTpNameNodeGen.Uncached@5009421e: [null, 3], [null,Integer]
CPython:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
calculateMetaclass (TypeBuiltins.java#L401-L423) checks IsAcceptableBaseNode on each base before comparing metaclasses, and raises TYPE_IS_NOT_ACCEPTABLE_BASE_TYPE, whose %N needs a type, for the int 3. Formatting the message calls GetTpNameNode on a non-type, which has no specialization for it. CPython's _PyType_CalculateMetaclass (typeobject.c#L3057-L3089) compares Py_TYPE(base) first, so a non-type base always gives the metaclass conflict error. Bases such as len, lambda: 0 or functools.partial(len) crash the same way.
Fix suggestion
In calculateMetaclass, move the IsAcceptableBaseNode check into a separate pass after the metaclass loop, as CPython runs best_base after _PyType_CalculateMetaclass.
3. type() puts __module__ after the __dict__ and __weakref__ descriptors
| Expression | GraalPy | CPython |
|---|---|---|
list(vars(type('C', (), {}))) |
['__dict__', '__weakref__', '__module__', '__doc__'] |
['__module__', '__dict__', '__weakref__', '__doc__'] |
CreateTypeNode.makeType (TypeNodes.java#L1865-L1876) creates the class, which installs the descriptors, and only then sets __module__. CPython sets __module__ (typeobject.c#L3674) before PyType_Ready adds the descriptors. A class statement is not affected because its namespace already holds __module__.
Fix suggestion
In makeType, put __module__ into namespace before calling typeMetaclass.execute().
4. NotImplementedType has no __bool__
import warnings
warnings.simplefilter('error')| Expression | GraalPy | CPython |
|---|---|---|
'__bool__' in dir(NotImplemented) |
False |
True |
bool(NotImplemented) |
True |
DeprecationWarning: NotImplemented should not be used in a boolean context |
NotImplementedBuiltins (NotImplementedBuiltins.java#L67-L107) defines no nb_bool. CPython's notimplemented_bool (object.c#L1962-L1975) warns and returns true.
Fix suggestion
Add an nb_bool slot to NotImplementedBuiltins that issues the DeprecationWarning and returns true.
5. frozenset() returns a frozenset subclass instance unchanged
class F(frozenset):
pass| Expression | GraalPy | CPython |
|---|---|---|
type(frozenset(F())) |
<class '__main__.F'> |
<class 'frozenset'> |
frozensetIdentity (FrozenSetBuiltins.java#L104-L109) returns the argument for any PFrozenSet. CPython takes that shortcut only for an exact frozenset (setobject.c#L995).
Fix suggestion
Guard frozensetIdentity on the argument being an exact frozenset, and let a subclass instance fall through to the copying specialization.
6. repr() of the smallest subnormals is not the shortest string
| Expression | GraalPy | CPython |
|---|---|---|
repr(5e-324) |
4.9e-324 |
5e-324 |
repr(1e-323) |
9.9e-324 |
1e-323 |
repr(5e-323) |
4.9e-323 |
5e-323 |
reprDigits (FloatFormatter.java#L775-L778) takes its digits from Double.toString, which picks the closest decimal of length 1 or 2 when one digit would round-trip. CPython's float_repr uses the shortest string that round-trips (floatobject.c#L391-L394).
Fix suggestion
In reprDigits, for a subnormal value use the nearest one-digit decimal (new BigDecimal(value).round(new MathContext(1, RoundingMode.HALF_EVEN))) when it round-trips. Simulated in plain Java, this matches CPython on the first 300,000 positive subnormals.
7. compile() raises TypeError for an out-of-range optimize
| Expression | GraalPy | CPython |
|---|---|---|
compile('1', 'f', 'exec', optimize=5) |
TypeError: compile(): invalid optimize value |
ValueError: compile(): invalid optimize value |
checkOptimize (BuiltinFunctions.java#L1137-L1142) raises TypeError. CPython raises ValueError (bltinmodule.c#L766-L770).
Fix suggestion
Raise ValueError in checkOptimize, as the neighbouring checkFlags does.
8. Three-argument pow() raises ValueError for operands complex cannot convert
| Expression | GraalPy | CPython |
|---|---|---|
pow(1, int, 1j) |
ValueError: complex modulo |
TypeError: unsupported operand type(s) for ** or pow(): 'int', 'type', 'complex' |
pow(1j, set(), 3) |
ValueError: complex modulo |
TypeError: unsupported operand type(s) for ** or pow(): 'complex', 'set', 'int' |
ComplexBuiltins.PowerNode.error (ComplexBuiltins.java#L1192-L1198) raises as soon as a modulus is given. CPython's complex_pow (complexobject.c#L507-L517) converts both operands first and returns NotImplemented when one is not a number.
Fix suggestion
In error, convert left and right with ToComplexValueNode and return NotImplemented when either fails, raising ValueError only otherwise.
9. list subclass + list returns the subclass
import operator
class L(list):
pass| Expression | GraalPy | CPython |
|---|---|---|
type(L([1]) + [2]) |
<class '__main__.L'> |
<class 'list'> |
type(operator.concat(L([1]), [2])) |
<class '__main__.L'> |
<class 'list'> |
ListBuiltins.ConcatNode allocates the result with the class from GetClassForNewListNode (ListNodes.java#L196-L204), which is the receiver's class. CPython's list_concat always builds an exact list (listobject.c#L531). The result skips L.__init__.
Fix suggestion
Have GetClassForNewListNode.doPList return PythonBuiltinClassType.PList, which also fixes L.copy().
10. bytearray item assignment checks the index before the value
b = bytearray(b'x')| Expression | GraalPy | CPython |
|---|---|---|
b[5] = 1j |
IndexError: bytearray index out of range |
TypeError: 'complex' object cannot be interpreted as an integer |
b[5] = 300 |
IndexError: bytearray index out of range |
ValueError: byte must be in range(0, 256) |
SetSubscriptNode.set and SetItemNode.set (ByteArrayBuiltins.java#L221-L263) bounds-check the index before the value is converted. CPython converts the value first (bytearrayobject.c#L606-L616, GH-91153).
Fix suggestion
In both set methods, convert value to a byte before normalizing the index, then store the converted byte.
11. math.gcd() and math.lcm() with one argument call abs() before __index__
import math
class Idx:
def __index__(self):
return 6| Expression | GraalPy | CPython |
|---|---|---|
math.gcd(Idx()) |
TypeError: bad operand type for abs(): 'Idx' |
6 |
math.lcm(Idx()) |
TypeError: bad operand type for abs(): 'Idx' |
6 |
The one-argument specializations GcdNode.gcdOne and LcmNode.gcdOne (MathModuleBuiltins.java#L969-L975, #L1125-L1131) both compute index(abs(x)). CPython computes abs(index(x)) (mathmodule.c#L767-L773, #L842-L848).
Fix suggestion
Swap the two calls in both GcdNode.gcdOne and LcmNode.gcdOne.
12. itertools.combinations() and combinations_with_replacement() check r before the iterable
| Expression | GraalPy | CPython |
|---|---|---|
itertools.combinations(1, -1) |
ValueError: r must be non-negative |
TypeError: 'int' object is not iterable |
itertools.combinations_with_replacement(1, -1) |
ValueError: r must be non-negative |
TypeError: 'int' object is not iterable |
Both constructors (CombinationsBuiltins.java#L134-L139, CombinationsWithReplacementBuiltins.java#L109-L113) raise for r < 0 before converting the iterable. CPython builds the pool first (itertoolsmodule.c#L2577-L2583, #L2893-L2899).
Fix suggestion
In both construct methods, call toArrayNode.execute(frame, iterable) before the r < 0 check, as PermutationsBuiltins does.
13. itertools.count() keeps an int subclass start
| Expression | GraalPy | CPython |
|---|---|---|
repr(itertools.count(True)) |
count(True) |
count(1) |
next(itertools.count(True)) |
True |
1 |
CountNode.construct (CountBuiltins.java#L114-L133) stores start as passed. CPython's fast mode (itertoolsmodule.c#L4028-L4067) keeps an int start with step 1 as a C Py_ssize_t, so it produces exact ints.
Fix suggestion
In construct, when start and step are int instances, step == 1 and start fits in a long, store start as an exact int.
14. functools.lru_cache() requires an integer typed
import functools| Expression | GraalPy | CPython |
|---|---|---|
functools.lru_cache(maxsize=2, typed='x')(len)('ab') |
TypeError: 'str' object cannot be interpreted as an integer |
2 |
LruCacheWrapperBuiltins converts typed with ClinicConversion.Int (LruCacheWrapperBuiltins.java#L123). CPython parses it with p (_functoolsmodule.c#L1168).
Fix suggestion
Use ClinicConversion.Boolean for typed and store typed ? 1 : 0.
15. bisect functions treat an explicit hi=-1 as an empty range
import bisect| Expression | GraalPy | CPython |
|---|---|---|
bisect.bisect_right([1, 2], 1, 0, -1) |
0 |
1 |
bisect.bisect_left([1, 2], 3, 0, -1) |
0 |
2 |
a = [1, 2]; bisect.insort_right(a, 3, 0, -1); a |
[3, 1, 2] |
[1, 2, 3] |
GraalPy has no _bisect, so bisect keeps the pure-Python functions from the bundled bisect.py (bisect.py#L111-L114), which replace only hi is None with len(a) (#L36, #L89). CPython runs the C _bisect, where -1 is the default of hi and becomes len(a) (_bisectmodule.c#L60-L64, #L244-L248). CPython declined to change its pure-Python version for this (python/cpython#125889).
Fix suggestion
Provide a native _bisect with the C argument handling, or treat hi == -1 like None in bisect_right and bisect_left of the bundled bisect.py.
Expected behavior
The CPython column of each item. CPython 3.12.13 and 3.13.16 give the same output for all of them.
Stack trace
Additional context
No response
Activity