Repository navigation
Description
Describe the bug
When the key's __eq__ advances the same group iterator while groupby is comparing keys (the case from bpo-30347), the outer next() on the group iterator returns a Java null instead of a value or exhaustion. Using that result terminates the process with a Java NullPointerException (exit status 1). CPython returns None here (the default given to next()).
Operating system
macOS
CPU architecture
ARM64
GraalPy version
25.4.4.1.1 (Python 3.13.14), community build from the GitHub release. The code involved is unchanged on master d0565ae.
JDK version
No response
Context configuration
No response
Steps to reproduce
from itertools import groupby
class K:
def __init__(self, v):
pass
def __eq__(self, other):
global i
i += 1
if i == 1:
next(g, None) # advance the same group iterator from inside __eq__
return True
i = 0
g = next(groupby(range(10), K))[1]
print(repr(next(g, None)))
print(repr(next(g, None)))
print("done")GraalPy prints 0, then exits with the NullPointerException below.
Expected behavior
CPython 3.13.18 (same on 3.12.16):
0
None
done
Stack trace
java.lang.NullPointerException: Null receiver values are not supported by libraries.
at org.graalvm.truffle/com.oracle.truffle.api.library.LibraryFactory.dispatch(LibraryFactory.java:550)
at org.graalvm.truffle/com.oracle.truffle.api.library.LibraryFactory.create(LibraryFactory.java:302)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetForeignObjectClassNodeGen.executeAndSpecialize(GetForeignObjectClassNodeGen.java:143)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetForeignObjectClassNodeGen.execute(GetForeignObjectClassNodeGen.java:96)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetRegisteredClassNode.noRegisteredClass(GetRegisteredClassNode.java:95)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetRegisteredClassNodeGen.executeAndSpecialize(GetRegisteredClassNodeGen.java:183)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetRegisteredClassNodeGen.execute(GetRegisteredClassNodeGen.java:157)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetClassNode.getForeign(GetClassNode.java:200)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetClassNodeGen$Inlined.executeAndSpecialize(GetClassNodeGen.java:476)
at org.graalvm.py/com.oracle.graal.python.nodes.object.GetClassNodeGen$Inlined.execute(GetClassNodeGen.java:393)
at org.graalvm.py/com.oracle.graal.python.builtins.objects.type.TpSlots$GetObjectSlotsNode.doIt(TpSlots.java:2085)
at org.graalvm.py/com.oracle.graal.python.builtins.objects.type.TpSlotsFactory$GetObjectSlotsNodeGen$Inlined.execute(TpSlotsFactory.java:342)
at org.graalvm.py/com.oracle.graal.python.lib.PyObjectReprAsObjectNode.repr(PyObjectReprAsObjectNode.java:105)
at org.graalvm.py/com.oracle.graal.python.lib.PyObjectReprAsObjectNodeGen$Inlined.execute(PyObjectReprAsObjectNodeGen.java:329)
at org.graalvm.py/com.oracle.graal.python.builtins.modules.BuiltinFunctions$ReprNode.repr(BuiltinFunctions.java:1711)
at org.graalvm.py/com.oracle.graal.python.builtins.modules.BuiltinFunctionsFactory$ReprNodeFactory$ReprNodeGen.execute(BuiltinFunctionsFactory.java:7030)Additional context
Assigning the result instead (x = next(g, None)) does not crash, but x is then unbound: the next line that reads it raises NameError: name 'x' is not defined.
test_itertools.RegressionTests.test_issue30347_2 passes because it discards the result of next(g, None).
Root Cause
GrouperBuiltins.NextNode reads gbo.getCurrValue() after the key comparison (GrouperBuiltins.java#L145-L155). The comparison calls the key's __eq__, and the nested next() there takes the current value and sets it to null, so the outer call returns that null. CPython's _grouper_next also reads currvalue after the comparison, but a NULL return without an exception set means exhaustion there (itertoolsmodule.c#L701-L734).
Fix Suggestion
In NextNode, treat a null current value after the comparison as exhaustion (throw iteratorExhausted()), as CPython does.
Activity