Skip to content

Commit 6764cb1

Browse files
committed
[ty] Prefer reflected operators by runtime class
1 parent 889ac31 commit 6764cb1

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

crates/ty_python_semantic/resources/mdtest/binary/instances.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,54 @@ class C(B): ...
218218
reveal_type(A() + C()) # revealed: MyString
219219
```
220220

221+
## Reflected precedence uses runtime classes
222+
223+
`IntFlag` values are commonly accumulated into a mask that starts at the integer zero. At runtime,
224+
the first `|=` produces a `Permission`, because reflected-method precedence depends on the operands'
225+
runtime classes. The enum literal is not a subtype of the specific integer literal `0`, but its
226+
runtime class is a strict subclass of `int`:
227+
228+
```py
229+
from enum import IntFlag, auto
230+
231+
class Permission(IntFlag):
232+
READ = auto()
233+
WRITE = auto()
234+
235+
def permissions_for(editable: bool) -> Permission:
236+
permissions = 0
237+
permissions |= Permission.READ
238+
reveal_type(permissions) # revealed: Literal[Permission.READ]
239+
240+
if editable:
241+
permissions |= Permission.WRITE
242+
243+
return permissions
244+
```
245+
246+
## Bounded TypeVars do not have an exact runtime class
247+
248+
The upper bound of a TypeVar is not necessarily its runtime class. The bound therefore cannot be
249+
used to decide whether the right-hand operand's reflected method takes precedence:
250+
251+
```py
252+
from typing import Literal, TypeVar
253+
254+
class Base:
255+
def __add__(self, other: object) -> Literal["base"]:
256+
return "base"
257+
258+
class Child(Base):
259+
def __radd__(self, other: object) -> Literal["child"]:
260+
return "child"
261+
262+
T = TypeVar("T", bound=Base)
263+
264+
def add_child(left: T) -> Literal["base"]:
265+
reveal_type(left + Child()) # revealed: Literal["base"]
266+
return left + Child()
267+
```
268+
221269
## Reflected precedence 2
222270

223271
If the right-hand operand is a subtype of the left-hand operand, but does not override the reflected

crates/ty_python_semantic/src/types/call.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,28 @@ impl<'db> Type<'db> {
6262
//
6363
// [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__
6464

65-
// Technically we don't have to check left_ty != right_ty here, since if the types
66-
// are the same, they will trivially have the same implementation of the reflected
67-
// dunder, and so we'll fail the inner check. But the type equality check will be
68-
// faster for the common case, and allow us to skip the (two) class member lookups.
65+
// Reflected-method precedence is based on the operands' runtime classes, not on whether
66+
// one value type is a subtype of the other. This matters for literals: an enum literal can
67+
// have a class that is a strict subclass of `int` even though it is not a subtype of a
68+
// specific integer literal.
69+
//
70+
// Technically, the type equality check is not required for correctness: equal value types
71+
// have the same runtime class and reflected implementation. It provides a fast path for
72+
// the common case and avoids the runtime-class checks and two class member lookups below.
73+
let right_is_strict_subclass = left_ty != right_ty
74+
&& match (left_ty.nominal_class(db), right_ty.nominal_class(db)) {
75+
(Some(left_class), Some(right_class))
76+
if !left_ty.is_type_var() && !right_ty.is_type_var() =>
77+
{
78+
left_class.class_literal(db) != right_class.class_literal(db)
79+
&& right_class.is_subclass_of(db, left_class)
80+
}
81+
_ => right_ty.is_subtype_of(db, left_ty),
82+
};
83+
6984
let left_class = left_ty.to_meta_type(db);
7085
let right_class = right_ty.to_meta_type(db);
71-
if left_ty != right_ty && right_ty.is_subtype_of(db, left_ty) {
86+
if right_is_strict_subclass {
7287
let reflected_dunder = op.reflected_dunder();
7388
let rhs_reflected = right_class.member(db, reflected_dunder).place;
7489
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible

0 commit comments

Comments
 (0)