Skip to content

Commit b7a3905

Browse files
authored
[mypyc] Fix compile error when a native __next__ returns an unboxed value (#22145)
Fixes mypyc/mypyc#1235. `native_slot()` filled `tp_iternext` with the native `__next__` method itself, which only works if the method returns `PyObject *`. With `-> int`, CPython got the tagged integer, or `CPY_INT_TAG` after `StopIteration`, as an object pointer, so `next()`, `for` loops, `list()` and `obj.__next__()` from Python all crashed. `float`, `i64` and tuples crashed too. With `bool`, `False` came back as NULL, so the iteration stopped early without an error. MSVC warns about the mismatch (C4047). `native_slot()` now uses `generate_dunder_wrapper()` when the method returns an unboxed value. That's the wrapper the unary number slots such as `__int__` and `__index__` already use. It calls the native method, returns NULL if the method raised (checking `PyErr_Occurred()` for types such as `float`, whose error value is also a valid value), and boxes the result. A `StopIteration` raised by `__next__` still ends the iteration, because the wrapper returns NULL with it set. Methods that return an object, including the `__next__` of every native generator, still fill the slot directly, so they don't get an extra call. The other slots that use `native_slot()` (`__iter__`, `__str__`, `__repr__`, `__await__`, `__aiter__` and `__anext__`) get the same check, although they normally return objects.
1 parent f501cd0 commit b7a3905

2 files changed

Lines changed: 183 additions & 0 deletions

File tree

‎mypyc/codegen/emitclass.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@
5353

5454

5555
def native_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
56+
"""Use a native method directly as a slot that takes self and returns an object.
57+
58+
If the method returns an unboxed value, such as an int from __next__, a wrapper
59+
that boxes the value fills the slot instead.
60+
"""
61+
if fn.ret_type.is_unboxed:
62+
return generate_dunder_wrapper(cl, fn, emitter)
5663
return f"{NATIVE_PREFIX}{fn.cname(emitter.names)}"
5764

5865

‎mypyc/test-data/run-dunders.test‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,182 @@ class InterpOverride(C):
409409
def __index__(self) -> int:
410410
return 2
411411

412+
[case testDundersNext]
413+
from typing import Any
414+
415+
from mypy_extensions import i64, mypyc_attr
416+
from testutil import assertRaises
417+
418+
@mypyc_attr(allow_interpreted_subclasses=True)
419+
class Counter:
420+
def __init__(self, n: int) -> None:
421+
self.i = 0
422+
self.n = n
423+
424+
def __iter__(self) -> "Counter":
425+
return self
426+
427+
def __next__(self) -> int:
428+
if self.i == self.n:
429+
raise StopIteration
430+
self.i += 1
431+
return self.i
432+
433+
class Inherit(Counter):
434+
pass
435+
436+
class Override(Counter):
437+
def __next__(self) -> int:
438+
return super().__next__() * 10
439+
440+
class Container:
441+
def __iter__(self) -> Counter:
442+
return Counter(3)
443+
444+
class BoolIter:
445+
def __init__(self, items: list[bool]) -> None:
446+
self.items = items
447+
448+
def __iter__(self) -> "BoolIter":
449+
return self
450+
451+
def __next__(self) -> bool:
452+
if not self.items:
453+
raise StopIteration
454+
return self.items.pop(0)
455+
456+
class FloatIter:
457+
def __init__(self, items: list[float], fail: bool = False) -> None:
458+
self.items = items
459+
self.fail = fail
460+
461+
def __iter__(self) -> "FloatIter":
462+
return self
463+
464+
def __next__(self) -> float:
465+
if not self.items:
466+
if self.fail:
467+
raise ValueError("bad float")
468+
raise StopIteration
469+
return self.items.pop(0)
470+
471+
class I64Iter:
472+
def __init__(self, items: list[i64]) -> None:
473+
self.items = items
474+
475+
def __iter__(self) -> "I64Iter":
476+
return self
477+
478+
def __next__(self) -> i64:
479+
if not self.items:
480+
raise StopIteration
481+
return self.items.pop(0)
482+
483+
class TupleIter:
484+
def __init__(self) -> None:
485+
self.items = [(1, "a"), (2, "b")]
486+
487+
def __iter__(self) -> "TupleIter":
488+
return self
489+
490+
def __next__(self) -> tuple[int, str]:
491+
if not self.items:
492+
raise StopIteration
493+
return self.items.pop(0)
494+
495+
class StrIter:
496+
def __init__(self) -> None:
497+
self.items = ["a", "b"]
498+
499+
def __iter__(self) -> "StrIter":
500+
return self
501+
502+
def __next__(self) -> str:
503+
if not self.items:
504+
raise StopIteration
505+
return self.items.pop(0)
506+
507+
class Raises:
508+
def __iter__(self) -> "Raises":
509+
return self
510+
511+
def __next__(self) -> int:
512+
raise ValueError("bad next")
513+
514+
def test_int() -> None:
515+
assert list(Counter(3)) == [1, 2, 3]
516+
c = Counter(2)
517+
assert next(c) == 1
518+
assert next(c) == 2
519+
with assertRaises(StopIteration):
520+
next(c)
521+
assert next(c, -1) == -1
522+
result = []
523+
for x in Counter(3):
524+
result.append(x)
525+
assert result == [1, 2, 3]
526+
assert [x * 2 for x in Counter(3)] == [2, 4, 6]
527+
assert list(Container()) == [1, 2, 3]
528+
529+
def test_int_generic() -> None:
530+
a: Any = Counter(3)
531+
assert next(a) == 1
532+
assert a.__next__() == 2
533+
assert list(a) == [3]
534+
with assertRaises(StopIteration):
535+
a.__next__()
536+
537+
def test_subclasses() -> None:
538+
assert list(Inherit(3)) == [1, 2, 3]
539+
assert list(Override(3)) == [10, 20, 30]
540+
541+
def test_other_unboxed_types() -> None:
542+
assert list(BoolIter([False, True, False])) == [False, True, False]
543+
assert list(BoolIter([True, False])) == [True, False]
544+
# -113.0 and -113 are the error values of float and i64.
545+
assert list(FloatIter([1.5, -113.0, 2.0])) == [1.5, -113.0, 2.0]
546+
assert list(I64Iter([1, -113, 2])) == [1, -113, 2]
547+
assert list(TupleIter()) == [(1, "a"), (2, "b")]
548+
549+
def test_boxed_type() -> None:
550+
assert list(StrIter()) == ["a", "b"]
551+
552+
def test_errors() -> None:
553+
with assertRaises(ValueError, "bad next"):
554+
list(Raises())
555+
with assertRaises(ValueError, "bad next"):
556+
next(Raises())
557+
with assertRaises(ValueError, "bad float"):
558+
list(FloatIter([1.0], fail=True))
559+
560+
def test_interpreted() -> None:
561+
from interp import InterpInherit, InterpOverride, call_next, consume
562+
563+
assert consume(Counter(3)) == [1, 2, 3]
564+
assert call_next(Counter(3)) == 1
565+
assert consume(BoolIter([False, True])) == [False, True]
566+
assert consume(TupleIter()) == [(1, "a"), (2, "b")]
567+
assert consume(InterpInherit(2)) == [1, 2]
568+
assert consume(InterpOverride(2)) == [101, 102]
569+
570+
[file interp.py]
571+
from typing import Any
572+
573+
from native import Counter
574+
575+
class InterpInherit(Counter):
576+
pass
577+
578+
class InterpOverride(Counter):
579+
def __next__(self) -> int:
580+
return super().__next__() + 100
581+
582+
def consume(it: Any) -> list[Any]:
583+
return [x for x in it]
584+
585+
def call_next(it: Any) -> Any:
586+
return it.__next__()
587+
412588
[case testDundersBinarySimple]
413589
from typing import Any
414590

0 commit comments

Comments
 (0)