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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type narrowing not done correctly for bitwise operations with int and instance of subclass of enum.IntFlag

1 participant