Skip to content

__reversed__ with an explicit Iterator[T] return annotation synthesizes IReverseEnumerable[Iterator[T]], not IReverseEnumerable[T] #1832

Description

@antonsynd

Measured on v5/clr-resolution (visible only once IReverseEnumerable[int] became nameable again — P4 remediation R4; at 311252e33 the annotation itself was SPY0202, so the mismatch was masked).

class Bag:
    _items: list[int]

    def __init__(self) -> None:
        self._items = [4, 5]

    def __reversed__(self) -> Iterator[int]:
        return reversed(self._items)

def take(r: IReverseEnumerable[int]) -> None:
    for x in r:
        print(x)

def main() -> None:
    take(Bag())
error[SPY0220]: Cannot pass argument of type 'Bag' to parameter of type 'IReverseEnumerable[int32]'

Changing the parameter to IReverseEnumerable[Iterator[int]] makes the argument bind (it then fails later on #1808, iterating an interface-typed receiver), so the synthesized interface's type argument is the __reversed__ return annotation verbatim rather than its element type.

The generator shape is correct: def __reversed__(self) -> int: yield ... synthesizes IReverseEnumerable[int] and take(c) binds (SynthesizedInterfaceVisibilityTests.Reversed_Class_ElementTypeFromAnnotation, and Integration/TestFixtures/interop/sharpy_interface_by_name_1625.spy). Only the non-generator arm — an explicit Iterator[T] / IEnumerator[T] return — takes the annotation unpeeled.

The element-type decision belongs to the one iterable-element decider (InferReversedElementType and friends), which should unwrap an iterator/iterable return the same way the generator arm does. Same family as the P9 ClassifyIterableSource consolidation.

Activity

  1. antonsynd commented on Sep 12, 2026

    @antonsynd
    OwnerAuthor

    Placement (2026-09-11 batching): P10, with #1850 and #1868 under the one iterable-element decider: every producer (a __reversed__/__iter__ return annotation) and consumer (for, comprehension, yield from, list()/set()/… constructors, reversed, sum/min/max, in) asks the same decider; a non-iterable is a named refusal at every consumer. Reproduced @ 41dd19d (SPY0220 Bag into IReverseEnumerable[int32]).

  2. antonsynd commented on Sep 19, 2026

    @antonsynd
    OwnerAuthor

    Closed by P10 (plan-f164be), Phase 3 — commits 8b6668b42 (IterableElementDecider + producers) + da71f0ca3 (emitter reads the materialized element) + 3b7a0293b (matrices/fixtures).

    Acceptance:

    • IterableElementDeciderConformanceTests 10/10; IterableProducerAnnotationMatrixTests green. Emitter annotation-map source scan pre-fix count 4 → 0 (all 4 arms via FindSynthesizedInterface/GetSynthesizedElementType). Mutation: restore MapType(funcDef.ReturnType) → c02c red while the slot cell stays green; remove the non-generator row → c03/b14 red.
    • By direction: c02c reversed(r) with __reversed__ -> Iterator[int] was SPY0908 CS0266, now prints 5 4; c02 element probe names int32; c03 e: IEnumerable[int] = Bag() binds and runs 1 2; list(Bag()) → [1, 2].

    Whole-solution gate GREEN 31390 passed / 0 failed / 3 skipped @ ed7a5fe (Core 2715, Cli 337, Lsp 1053 incl. FrontEndParity, Stdlib 3773, Compiler.Tests 23512/0/3 incl. the 3 GapDiscovery sweeps).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions