From 9cadc1b41212778bfedbe78aacac8e941c3178f0 Mon Sep 17 00:00:00 2001 From: Jeongseop Lim Date: Sat, 8 Aug 2026 12:30:39 +0900 Subject: [PATCH 1/2] Fix enumerate() start argument handling The tp_new specializations do not separate an omitted start from an explicit start=None: - doNone dispatched on PNone, which covers both PNone.NO_VALUE (argument omitted) and PNone.NONE (explicit start=None), so enumerate(it, None) silently used 0. CPython defaults start to 0 only when the argument is omitted; anything passed goes through PyNumber_Index and raises. - The rejecting specialization matched PNone.NO_VALUE too. Since the specializations are shared across call sites, guarding only doNone would make a later call that omitted start take that branch and raise instead of using 0, so both guards are needed. Signed-off-by: Jeongseop Lim --- .../src/tests/test_enumerate_start.py | 61 +++++++++++++++++++ .../objects/enumerate/EnumerateBuiltins.java | 7 ++- 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py b/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py new file mode 100644 index 0000000000..10533e9cbe --- /dev/null +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py @@ -0,0 +1,61 @@ +# Copyright (c) 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 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must be included in all copies or substantial +# portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +def assert_raises(err, fn, *args, **kwargs): + raised = False + try: + fn(*args, **kwargs) + except err: + raised = True + assert raised + + +def test_explicit_none_start(): + # GH-1074: start defaults to 0 only when the argument is omitted; an + # explicitly passed None goes through PyNumber_Index and raises + assert_raises(TypeError, enumerate, 'abc', None) + assert_raises(TypeError, enumerate, 'abc', start=None) + + +def test_omitted_start_after_rejected_start(): + # GH-1074: the specializations are shared, so rejecting a bad start must + # not break a later call that omits it + assert_raises(TypeError, enumerate, 'abc', 'x') + assert list(enumerate('abc')) == [(0, 'a'), (1, 'b'), (2, 'c')] diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java index e927067fcf..6e61d9459b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.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. @@ -83,7 +83,7 @@ protected List> getNodeFa @GenerateNodeFactory public abstract static class EnumerateNode extends PythonBuiltinNode { - @Specialization + @Specialization(guards = "isNoValue(keywordArg)") static PEnumerate doNone(VirtualFrame frame, Object cls, Object iterable, @SuppressWarnings("unused") PNone keywordArg, @Bind Node inliningTarget, @Shared("getIter") @Cached PyObjectGetIter getIter, @@ -119,7 +119,8 @@ static boolean isIntegerIndex(Object idx) { return isInteger(idx) || idx instanceof PInt; } - @Specialization(guards = "!isIntegerIndex(start)") + // !isNoValue: specializations are shared, so an omitted start must not reach this branch + @Specialization(guards = {"!isIntegerIndex(start)", "!isNoValue(start)"}) static void enumerate(@SuppressWarnings("unused") Object cls, @SuppressWarnings("unused") Object iterable, Object start, @Bind Node inliningTarget) { throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.OBJ_CANNOT_BE_INTERPRETED_AS_INTEGER, start); From cd487bc5043b685d3e24d359531d1bbd35c88c62 Mon Sep 17 00:00:00 2001 From: Jeongseop Lim Date: Sat, 8 Aug 2026 12:34:52 +0900 Subject: [PATCH 2/2] Coerce the enumerate() start argument through __index__ The tp_new specialization set decided whether a start argument was acceptable with a Java type test: isIntegerIndex admits only Integer, Long and PInt, and everything else was rejected without the object being consulted. So bool and any object implementing __index__ raised TypeError. CPython runs a start it was actually given through PyNumber_Index, which takes bool on the PyLong_Check fast path and calls nb_index on anything else providing it. range already does this in GraalPy, via PyNumberIndexNode; enumerate now does too, with the coercion running before the iterator is acquired so argument validation keeps CPython's order. Signed-off-by: Jeongseop Lim --- .../src/tests/test_enumerate_start.py | 34 +++++++++++++++++++ .../objects/enumerate/EnumerateBuiltins.java | 26 ++++++++++---- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py b/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py index 10533e9cbe..800ae1cb80 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_enumerate_start.py @@ -59,3 +59,37 @@ def test_omitted_start_after_rejected_start(): # not break a later call that omits it assert_raises(TypeError, enumerate, 'abc', 'x') assert list(enumerate('abc')) == [(0, 'a'), (1, 'b'), (2, 'c')] + + +def test_bool_start(): + # GH-1074: bool is an int subclass, so True is a start of 1 + assert list(enumerate('abc', True)) == [(1, 'a'), (2, 'b'), (3, 'c')] + assert list(enumerate('abc', False)) == [(0, 'a'), (1, 'b'), (2, 'c')] + + +def test_index_start(): + # GH-1074: any object implementing __index__ is accepted as start + class Idx: + def __index__(self): + return 3 + + assert list(enumerate([9, 8, 7], Idx())) == [(3, 9), (4, 8), (5, 7)] + + +def test_huge_index_start(): + # GH-1074: an __index__ result too large for a Java long still works + class BigIdx: + def __index__(self): + return 2 ** 70 + + assert list(enumerate('a', BigIdx())) == [(1180591620717411303424, 'a')] + + +def test_float_start_is_rejected(): + assert_raises(TypeError, enumerate, 'abc', 1.0) + + +def test_omitted_start_after_coerced_start(): + # GH-1074: coercing a start must not break a later call that omits it + assert list(enumerate('abc', True)) == [(1, 'a'), (2, 'b'), (3, 'c')] + assert list(enumerate('abc')) == [(0, 'a'), (1, 'b'), (2, 'c')] diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java index 6e61d9459b..6742a38b9d 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/enumerate/EnumerateBuiltins.java @@ -29,7 +29,6 @@ import static com.oracle.graal.python.nodes.PGuards.isInteger; import static com.oracle.graal.python.nodes.SpecialMethodNames.J___CLASS_GETITEM__; import static com.oracle.graal.python.nodes.SpecialMethodNames.J___REDUCE__; -import static com.oracle.graal.python.runtime.exception.PythonErrorType.TypeError; import java.util.List; @@ -49,14 +48,14 @@ import com.oracle.graal.python.builtins.objects.type.TypeNodes; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotIterNext.CallSlotTpIterNextNode; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotIterNext.TpIterNextBuiltin; +import com.oracle.graal.python.lib.PyNumberIndexNode; import com.oracle.graal.python.lib.PyObjectGetIter; -import com.oracle.graal.python.nodes.ErrorMessages; -import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.PythonBuiltinNode; 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.nodes.util.CastToJavaLongExactNode; import com.oracle.graal.python.runtime.object.PFactory; import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; @@ -66,6 +65,7 @@ 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.object.Shape; import com.oracle.truffle.api.profiles.InlinedConditionProfile; @CoreFunctions(extendClasses = PythonBuiltinClassType.PEnumerate) @@ -84,7 +84,7 @@ protected List> getNodeFa public abstract static class EnumerateNode extends PythonBuiltinNode { @Specialization(guards = "isNoValue(keywordArg)") - static PEnumerate doNone(VirtualFrame frame, Object cls, Object iterable, @SuppressWarnings("unused") PNone keywordArg, + static PEnumerate doNoValue(VirtualFrame frame, Object cls, Object iterable, @SuppressWarnings("unused") PNone keywordArg, @Bind Node inliningTarget, @Shared("getIter") @Cached PyObjectGetIter getIter, @Shared @Cached TypeNodes.GetInstanceShape getInstanceShape) { @@ -119,11 +119,23 @@ static boolean isIntegerIndex(Object idx) { return isInteger(idx) || idx instanceof PInt; } + // see cpython://Objects/enumobject.c#enum_new_impl // !isNoValue: specializations are shared, so an omitted start must not reach this branch @Specialization(guards = {"!isIntegerIndex(start)", "!isNoValue(start)"}) - static void enumerate(@SuppressWarnings("unused") Object cls, @SuppressWarnings("unused") Object iterable, Object start, - @Bind Node inliningTarget) { - throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.OBJ_CANNOT_BE_INTERPRETED_AS_INTEGER, start); + static PEnumerate doGeneric(VirtualFrame frame, Object cls, Object iterable, Object start, + @Bind Node inliningTarget, + @Shared("getIter") @Cached PyObjectGetIter getIter, + @Shared @Cached TypeNodes.GetInstanceShape getInstanceShape, + @Cached PyNumberIndexNode indexNode, + @Cached CastToJavaLongExactNode cast, + @Cached InlinedConditionProfile bigIntIndexProfile) { + Object index = indexNode.execute(frame, inliningTarget, start); + Object iterator = getIter.execute(frame, inliningTarget, iterable); + Shape shape = getInstanceShape.execute(cls); + if (bigIntIndexProfile.profile(inliningTarget, index instanceof PInt)) { + return PFactory.createEnumerate(cls, shape, iterator, (PInt) index); + } + return PFactory.createEnumerate(cls, shape, iterator, cast.execute(inliningTarget, index)); } }