Skip to content

Try reflected binary-op dunder first for proper subclasses (#3876)#3982

Open
durvesh1992 wants to merge 1 commit into
facebook:mainfrom
durvesh1992:fix/reflected-dunder-subclass-priority
Open

Try reflected binary-op dunder first for proper subclasses (#3876)#3982
durvesh1992 wants to merge 1 commit into
facebook:mainfrom
durvesh1992:fix/reflected-dunder-subclass-priority

Conversation

@durvesh1992

Copy link
Copy Markdown

Summary

Fixes #3876.

Per Python's data model, when the right operand's type is a proper subclass of the left operand's type, the reflected dunder is tried first. Pyrefly always tried the forward dunder first, so for example:

from enum import IntFlag
class Color(IntFlag):
    RED = 1
    GREEN = 2

def f(x: int, c: Color):
    reveal_type(x & c)  # was: int   — should be: Color

int & Color resolved through int.__and__ (which widens the result back to int) instead of Color.__rand__ (which returns the flag type Self). At runtime int_val & color calls Color.__rand__ and yields a Color.

Fix

In binop_types' binop_call (pyrefly/lib/alt/operators.rs), when the RHS type is a proper subclass of the LHS type, the reflected dunder is now tried first.

This is intentionally narrow:

  • It only reorders when rhs is a strict subclass of lhs (has_superclass + not equal).
  • A subclass that does not override the reflected dunder inherits it unchanged, and try_binop_calls still falls back to the forward dunder if the reflected call doesn't apply — so non-overriding subclasses are unaffected.

Test

Added test_reflected_dunder_subclass_priority in pyrefly/lib/test/operators.rs asserting x & c, x | c, x ^ c are Color for x: int, c: Color. It fails before this change (inferred int) and passes after.

Python's data model calls the reflected dunder first when the right operand's
type is a proper subclass of the left operand's type. Pyrefly always tried the
forward dunder first, so `int_val & some_IntFlag_member` resolved through
`int.__and__` and widened the result back to `int` instead of keeping the flag
type via `IntFlag.__rand__` (facebook#3876).

Reorder the binary-op candidates so the reflected dunder is tried first in that
subclass case. A subclass that does not override the reflected dunder inherits
it unchanged and try_binop_calls still falls back to the forward dunder, so
non-overriding subclasses are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

1 participant