Skip to content

Fix enumerate() start handling for None and __index__ arguments - #1075

Open
jseop-lim wants to merge 2 commits into
oracle:masterfrom
jseop-lim:GH1074-enumerate-start
Open

Fix enumerate() start handling for None and __index__ arguments#1075
jseop-lim wants to merge 2 commits into
oracle:masterfrom
jseop-lim:GH1074-enumerate-start

Conversation

@jseop-lim

Copy link
Copy Markdown
Contributor

Description

Fixes #1074

enumerate() mishandles its start argument in two independent ways. An explicit start=None is treated as if the argument had been omitted, so it silently means 0. And a start that is not already an int is rejected instead of being coerced, so bool and every object implementing __index__ raise TypeError.

AS-IS

>>> enumerate("a", None)
<enumerate object at 0x2671a8f>
>>> list(enumerate("abc", True))
TypeError: 'bool' object cannot be interpreted as an integer
>>> list(enumerate([9, 8, 7], Three()))          # Three.__index__ returns 3
TypeError: 'Three' object cannot be interpreted as an integer

TO-BE

>>> enumerate("a", None)
TypeError: 'NoneType' object cannot be interpreted as an integer
>>> list(enumerate("abc", True))
[(1, 'a'), (2, 'b'), (3, 'c')]
>>> list(enumerate([9, 8, 7], Three()))
[(3, 9), (4, 8), (5, 7)]

(matching CPython)

The two halves ship together because the specializations are shared across call sites. An omitted start arrives as PNone.NO_VALUE, which is neither an integer index nor distinguishable from PNone.NONE by Java type, so guarding one specialization without the other moves the failure rather than removing it:

call sequence in one process before default-branch guard only both guards
enumerate("a", None) enumerate object TypeError TypeError
then enumerate("abc") [(0, 'a'), (1, 'b'), (2, 'c')] TypeError — regression [(0, 'a'), (1, 'b'), (2, 'c')]

Each column is a build. The same branch is already reachable before this change by rejecting a bad start first — enumerate("abc", "x") in a try, then enumerate("abc").

range already coerces its arguments through PyNumberIndexNode and separates an omitted argument with isNoValue guards, so this brings enumerate to the same shape.

Changes

  • Guard the specialization that defaults start to 0 with isNoValue(keywordArg) so it matches only an omitted argument, and rename it doNoValue to match.
  • Replace the rejecting specialization with doGeneric, which coerces start through PyNumberIndexNode and CastToJavaLongExactNode before the iterator is acquired. Its failure path raises the same ErrorMessages.OBJ_CANNOT_BE_INTERPRETED_AS_INTEGER as before, so None and float keep their exact message.
  • Add !isNoValue(start) to that specialization so an omitted argument can never reach it.
  • Add tests/test_enumerate_start.py covering the explicit None, bool, an __index__ implementer, one whose __index__ overflows a long, a rejected float, and an omitted start following each of the rejected and coerced cases.

Testing

mx build on linux-aarch64, then mx graalpytest test_enumerate_start.py test_enumerate.py test_list.py test_tuple.py test_iterator.py — 243 tests, all pass. CPython's own test.test_enumerate from lib-python passes as well (92 tests, 14 skipped).

Twelve call shapes were compared against CPython 3.12.10 on the built binary, including the sequences above that exercise the shared specialization state and an __index__ that returns 2 ** 70; all twelve agree. The table above was measured by building the unpatched tree, the one-guard variant, and the full change, and running the sequence against each.

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 <jeongseop_lim@korea.ac.kr>
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 <jeongseop_lim@korea.ac.kr>
@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: enumerate() ignores an explicit start=None and rejects __index__ starts

1 participant