From eeae71871d7ed9de5cf9b073201f71ac7ce7d510 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 17 Oct 2024 11:31:38 -0700 Subject: [PATCH 01/10] [red-knot] WIP: binary arithmetic on instances Co-authored-by: Alex Waygood --- .../resources/mdtest/binary/instances.md | 264 ++++++++++++++++++ .../resources/mdtest/binary/integers.md | 4 +- .../src/types/infer.rs | 65 +++++ crates/ruff_python_ast/src/nodes.rs | 36 +++ 4 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/binary/instances.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md new file mode 100644 index 0000000000000..84d0549f97831 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -0,0 +1,264 @@ +# Binary operations on instances + +Binary operations in Python are implemented by means of magic double-underscore methods. + +For references, see: + +- +- + +## Operations + +We support inference for all Python's binary operators: +`+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`, `<<`, `>>`, `&`, `^`, and `|`. + +```py +class A: + def __add__(self, other) -> A: return self + def __sub__(self, other) -> A: return self + def __mul__(self, other) -> A: return self + def __matmul__(self, other) -> A: return self + def __truediv__(self, other) -> A: return self + def __floordiv__(self, other) -> A: return self + def __mod__(self, other) -> A: return self + def __pow__(self, other) -> A: return self + def __lshift__(self, other) -> A: return self + def __rshift__(self, other) -> A: return self + def __and__(self, other) -> A: return self + def __xor__(self, other) -> A: return self + def __or__(self, other) -> A: return self + +class B: pass + +reveal_type(A() + B()) # revealed: A +reveal_type(A() - B()) # revealed: A +reveal_type(A() * B()) # revealed: A +reveal_type(A() @ B()) # revealed: A +reveal_type(A() / B()) # revealed: A +reveal_type(A() // B()) # revealed: A +reveal_type(A() % B()) # revealed: A +reveal_type(A() ** B()) # revealed: A +reveal_type(A() << B()) # revealed: A +reveal_type(A() >> B()) # revealed: A +reveal_type(A() & B()) # revealed: A +reveal_type(A() ^ B()) # revealed: A +reveal_type(A() | B()) # revealed: A +``` + +## Reflected + +We also support inference for reflected operations: + +```py +class A: + def __radd__(self, other) -> A: return self + def __rsub__(self, other) -> A: return self + def __rmul__(self, other) -> A: return self + def __rmatmul__(self, other) -> A: return self + def __rtruediv__(self, other) -> A: return self + def __rfloordiv__(self, other) -> A: return self + def __rmod__(self, other) -> A: return self + def __rpow__(self, other) -> A: return self + def __rlshift__(self, other) -> A: return self + def __rrshift__(self, other) -> A: return self + def __rand__(self, other) -> A: return self + def __rxor__(self, other) -> A: return self + def __ror__(self, other) -> A: return self + +class B: pass + + +reveal_type(B() + A()) # revealed: A +reveal_type(B() - A()) # revealed: A +reveal_type(B() * A()) # revealed: A +reveal_type(B() @ A()) # revealed: A +reveal_type(B() / A()) # revealed: A +reveal_type(B() // A()) # revealed: A +reveal_type(B() % A()) # revealed: A +reveal_type(B() ** A()) # revealed: A +reveal_type(B() << A()) # revealed: A +reveal_type(B() >> A()) # revealed: A +reveal_type(B() & A()) # revealed: A +reveal_type(B() ^ A()) # revealed: A +reveal_type(B() | A()) # revealed: A +``` + +## Return different type + +The magic methods aren't required to return the type of `self`: + +```py +class A: + def __add__(self, other) -> int: return 1 + def __rsub__(self, other) -> int: return 1 + +reveal_type(A() + "foo") # revealed: int +reveal_type("foo" - A()) # revealed: int +``` + +## Reflected precedence + +If the right-hand operand is a subtype of the left-hand operand and has a +different implementation of the reflected method, the reflected method on the +right-hand operand takes precedence. + +```py +class A: + def __add__(self, other) -> str: return "foo" + +class B(A): + def __radd__(self, other) -> int: return 42 + +reveal_type(A() + B()) # revealed: int +``` + +## Reflected precedence 2 + +If the right-hand operand is a subtype of the left-hand operand, but does not +override the reflected method, the left-hand operand's non-reflected method +still takes precedence: + +```py +class A: + def __add__(self, other) -> str: return "foo" + def __radd__(self, other) -> int: return 42 + +class B(A): pass + +reveal_type(A() + B()) # revealed: str +``` + +## Only reflected supported + +For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but +`(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime +(producing `2.2`). + +```py +class A: + def __sub__(self, other: A) -> A: + return A() + +class B: + def __rsub__(self, other: A) -> B: + return B() + +A() - B() +``` + +## Callable instances as dunders + +Believe it or not, this is supported at runtime: + +```py +class A: + def __call__(self, other) -> int: + return 42 + +class B: + __add__ = A() + +reveal_type(B() + B()) # revealed: int +``` + +## Integration test: numbers from typeshed + +```py +# TODO should be complex, need to check arg type and fall back +reveal_type(3.14 + 3j) # revealed: float +reveal_type(3j + 3.14) # revealed: complex +# TODO should be float, need to check arg type and fall back +reveal_type(42 + 4.2) # revealed: int +reveal_type(4.2 + 42) # revealed: float +reveal_type(3j + 3) # revealed: complex +# TODO should be complex, need to check arg type and fall back +reveal_type(3 + 3j) # revealed: int + +def returns_int() -> int: + return 42 + +def returns_bool() -> bool: + return True + +x = returns_bool + +reveal_type(returns_bool() + returns_int()) # revealed: int +# TODO should be float, need to check arg type and fall back +reveal_type(returns_int() + 4.12) # revealed: int +reveal_type(4.2 + returns_bool()) # revealed: float +``` + +## With literal types + +When we have a literal type for one operand, we're able to fall back to the +instance handling for its instance super-type. + +```py +class A: + def __add__(self, other) -> A: return self + def __radd__(self, other) -> A: return self + +reveal_type(A() + 1) # revealed: A +reveal_type(1 + A()) # revealed: int +reveal_type(A() + "foo") # revealed: A +# TODO should be A since `str.__add__` doesn't support A instances +reveal_type("foo" + A()) # revealed: @Todo +reveal_type(A() + b"foo") # revealed: A +reveal_type(b"foo" + A()) # revealed: bytes +reveal_type(A() + ()) # revealed: A +# TODO this should be A, since tuple's `__add__` doesn't support A instances +reveal_type(() + A()) # revealed: @Todo +``` + +## Unsupported + +### Dunder on instance + +The magic method must be on the class, not just on the instance: + +```py +def add_impl(self, other) -> int: return 1 + +class A: + def __init__(self): + self.__add__ = add_impl + +a = A() + +# error: [operator] +# revealed: Unknown +reveal_type(a + 1) +``` + +### Missing dunder + +```py +class A: pass + +# error: [operator] +# revealed: Unknown +reveal_type(A() + A()) +``` + +### Wrong position + +A left-hand dunder method doesn't apply for the right-hand operand, or vice versa: + +```py +class A: + def __add__(self, other) -> int: ... + +class B: + def __radd__(self, other) -> int: ... + +# error: [operator] +# revealed: Unknown +reveal_type(1 + A()) +# error: [operator] +# revealed: Unknown +reveal_type(B() + 1) +``` + +### Wrong type + +TODO: check signature and error if `other` is the wrong type diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md index 5c6296fb0768d..7b4a67381534e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -42,6 +42,6 @@ e = 1.0 / 0 reveal_type(a) # revealed: float reveal_type(b) # revealed: int reveal_type(c) # revealed: int -reveal_type(d) # revealed: @Todo -reveal_type(e) # revealed: @Todo +reveal_type(d) # revealed: float +reveal_type(e) # revealed: float ``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 02e91aa14195f..8817689014d28 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2489,6 +2489,15 @@ impl<'db> TypeInferenceBuilder<'db> { self.check_division_by_zero(binary, left_ty); } + self.infer_binary_expression_type(left_ty, right_ty, *op) + } + + fn infer_binary_expression_type( + &mut self, + left_ty: Type<'db>, + right_ty: Type<'db>, + op: ast::Operator, + ) -> Type<'db> { match (left_ty, right_ty, op) { // When interacting with Todo, Any and Unknown should propagate (as if we fix this // `Todo` in the future, the result would then become Any or Unknown, respectively.) @@ -2581,6 +2590,62 @@ impl<'db> TypeInferenceBuilder<'db> { } } + (Type::Instance(_), Type::IntLiteral(_), op) => { + self.infer_binary_expression_type(left_ty, KnownClass::Int.to_instance(self.db), op) + } + + (Type::IntLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( + KnownClass::Int.to_instance(self.db), + right_ty, + op, + ), + + (Type::Instance(_), Type::Tuple(_), op) => self.infer_binary_expression_type( + left_ty, + KnownClass::Tuple.to_instance(self.db), + op, + ), + + (Type::Tuple(_), Type::Instance(_), op) => self.infer_binary_expression_type( + KnownClass::Tuple.to_instance(self.db), + right_ty, + op, + ), + + (Type::Instance(_), Type::StringLiteral(_), op) => { + self.infer_binary_expression_type(left_ty, KnownClass::Str.to_instance(self.db), op) + } + + (Type::StringLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( + KnownClass::Str.to_instance(self.db), + right_ty, + op, + ), + + (Type::Instance(_), Type::BytesLiteral(_), op) => self.infer_binary_expression_type( + left_ty, + KnownClass::Bytes.to_instance(self.db), + op, + ), + + (Type::BytesLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( + KnownClass::Bytes.to_instance(self.db), + right_ty, + op, + ), + + (Type::Instance(left_class), Type::Instance(right_class), op) => left_class + .class_member(self.db, op.dunder()) + .call(self.db, &[left_ty, right_ty]) + .return_ty(self.db) + .unwrap_or_else(|| { + right_class + .class_member(self.db, op.reflected_dunder()) + .call(self.db, &[right_ty, left_ty]) + .return_ty(self.db) + .unwrap_or(Type::Unknown) + }), + _ => Type::Todo, // TODO } } diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index c8d508d2734ba..f44052f9fc4b4 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2971,6 +2971,42 @@ impl Operator { Operator::FloorDiv => "//", } } + + pub const fn dunder(self) -> &'static str { + match self { + Operator::Add => "__add__", + Operator::Sub => "__sub__", + Operator::Mult => "__mul__", + Operator::MatMult => "__matmul__", + Operator::Div => "__truediv__", + Operator::Mod => "__mod__", + Operator::Pow => "__pow__", + Operator::LShift => "__lshift__", + Operator::RShift => "__rshift__", + Operator::BitOr => "__or__", + Operator::BitXor => "__xor__", + Operator::BitAnd => "__and__", + Operator::FloorDiv => "__floordiv__", + } + } + + pub const fn reflected_dunder(self) -> &'static str { + match self { + Operator::Add => "__radd__", + Operator::Sub => "__rsub__", + Operator::Mult => "__rmul__", + Operator::MatMult => "__rmatmul__", + Operator::Div => "__rtruediv__", + Operator::Mod => "__rmod__", + Operator::Pow => "__rpow__", + Operator::LShift => "__rlshift__", + Operator::RShift => "__rrshift__", + Operator::BitOr => "__ror__", + Operator::BitXor => "__rxor__", + Operator::BitAnd => "__rand__", + Operator::FloorDiv => "__rfloordiv__", + } + } } impl fmt::Display for Operator { From 026c207d5034a91803f20141b28a704bfde10b42 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 17 Oct 2024 19:48:57 +0100 Subject: [PATCH 02/10] fix some bugs in tests and add a few more --- .../resources/mdtest/binary/instances.md | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 84d0549f97831..fb9b4bc025b92 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -66,7 +66,6 @@ class A: def __ror__(self, other) -> A: return self class B: pass - reveal_type(B() + A()) # revealed: A reveal_type(B() - A()) # revealed: A @@ -92,11 +91,30 @@ class A: def __add__(self, other) -> int: return 1 def __rsub__(self, other) -> int: return 1 -reveal_type(A() + "foo") # revealed: int -reveal_type("foo" - A()) # revealed: int +class B: + pass + +reveal_type(A() + B()) # revealed: int +reveal_type(B() - A()) # revealed: int ``` -## Reflected precedence +## Non-reflected precedence in general + +In general, if the left-hand side defines `__add__` and the right-hand side +defines `__radd__` and the right-hand side is not a subtype of the left-hand +side, `lhs.__add__` will take precedence: + +```py +class A: + def __add__(self, other: B) -> int: return 42 + +class B: + def __radd__(self, other: A) -> str: return "foo" + +reveal_type(A() + B()) # revealed: int +``` + +## Reflected precedence for subtypes (in some cases) If the right-hand operand is a subtype of the left-hand operand and has a different implementation of the reflected method, the reflected method on the @@ -105,11 +123,14 @@ right-hand operand takes precedence. ```py class A: def __add__(self, other) -> str: return "foo" + def __radd__(self, other) -> str: return "foo" + +class MyString(str): pass class B(A): - def __radd__(self, other) -> int: return 42 + def __radd__(self, other) -> MyString: return MyString() -reveal_type(A() + B()) # revealed: int +reveal_type(A() + B()) # revealed: MyString ``` ## Reflected precedence 2 @@ -164,13 +185,16 @@ reveal_type(B() + B()) # revealed: int ## Integration test: numbers from typeshed ```py +reveal_type(3j + 3.14) # revealed: complex +reveal_type(4.2 + 42) # revealed: float +reveal_type(3j + 3) # revealed: complex + # TODO should be complex, need to check arg type and fall back reveal_type(3.14 + 3j) # revealed: float -reveal_type(3j + 3.14) # revealed: complex + # TODO should be float, need to check arg type and fall back reveal_type(42 + 4.2) # revealed: int -reveal_type(4.2 + 42) # revealed: float -reveal_type(3j + 3) # revealed: complex + # TODO should be complex, need to check arg type and fall back reveal_type(3 + 3j) # revealed: int @@ -180,12 +204,14 @@ def returns_int() -> int: def returns_bool() -> bool: return True -x = returns_bool +x = returns_bool() +y = returns_int() + +reveal_type(x + y) # revealed: int +reveal_type(4.2 + x) # revealed: float -reveal_type(returns_bool() + returns_int()) # revealed: int # TODO should be float, need to check arg type and fall back -reveal_type(returns_int() + 4.12) # revealed: int -reveal_type(4.2 + returns_bool()) # revealed: float +reveal_type(y + 4.12) # revealed: int ``` ## With literal types @@ -223,11 +249,9 @@ class A: def __init__(self): self.__add__ = add_impl -a = A() - # error: [operator] # revealed: Unknown -reveal_type(a + 1) +reveal_type(A() + A()) ``` ### Missing dunder @@ -251,12 +275,15 @@ class A: class B: def __radd__(self, other) -> int: ... +class C: pass + # error: [operator] # revealed: Unknown -reveal_type(1 + A()) +reveal_type(C() + A()) + # error: [operator] # revealed: Unknown -reveal_type(B() + 1) +reveal_type(B() + C()) ``` ### Wrong type From ea515bda9742178f724359595b2d5978c2bf1bfd Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 18 Oct 2024 14:17:06 +0100 Subject: [PATCH 03/10] more tests --- .../resources/mdtest/binary/instances.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index fb9b4bc025b92..96f3b84fc474b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -111,7 +111,14 @@ class A: class B: def __radd__(self, other: A) -> str: return "foo" +# C is a subtype of C, but if the two sides are of equal types, +# the lhs *still* takes precedence +class C: + def __add__(self, other: C) -> int: return 42 + def __radd__(self, other: C) -> str: return "foo" + reveal_type(A() + B()) # revealed: int +reveal_type(C() + C()) # revealed: int ``` ## Reflected precedence for subtypes (in some cases) @@ -125,12 +132,17 @@ class A: def __add__(self, other) -> str: return "foo" def __radd__(self, other) -> str: return "foo" -class MyString(str): pass +class MyString(str): ... class B(A): def __radd__(self, other) -> MyString: return MyString() reveal_type(A() + B()) # revealed: MyString + +# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__` +class C(B): ... + +reveal_type(A() + C()) # revealed: MyString ``` ## Reflected precedence 2 From 95a6163526bd3ac6fe22343599a50ef9a1190989 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 18 Oct 2024 17:18:02 +0100 Subject: [PATCH 04/10] Fix precedence for reflected dunders if the r.h.s. is a subtype that overrides the reflected dunder --- .../resources/mdtest/binary/instances.md | 6 ++- crates/red_knot_python_semantic/src/types.rs | 3 ++ .../src/types/infer.rs | 41 ++++++++++++++----- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 96f3b84fc474b..29284c1252025 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -142,7 +142,11 @@ reveal_type(A() + B()) # revealed: MyString # N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__` class C(B): ... -reveal_type(A() + C()) # revealed: MyString +# TODO: we currently only understand direct subclasses as subtypes of the superclass. +# We need to iterate through the full MRO rather than just the class's bases; +# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being +# `MyString` rather than `str` +reveal_type(A() + C()) # revealed: str ``` ## Reflected precedence 2 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 070bcbe127029..89b735c4f5aea 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -440,6 +440,9 @@ impl<'db> Type<'db> { .any(|&elem_ty| ty.is_subtype_of(db, elem_ty)), (_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true, (Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false, + (Type::Instance(self_class), Type::Instance(target_class)) => self_class + .bases(db) // TODO: this should iterate through the MRO, not through the bases + .any(|base| base == Type::Class(target_class)), // TODO _ => false, } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 3a8994e6cd1d1..a41b58da4aa31 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2642,17 +2642,36 @@ impl<'db> TypeInferenceBuilder<'db> { op, ), - (Type::Instance(left_class), Type::Instance(right_class), op) => left_class - .class_member(self.db, op.dunder()) - .call(self.db, &[left_ty, right_ty]) - .return_ty(self.db) - .unwrap_or_else(|| { - right_class - .class_member(self.db, op.reflected_dunder()) - .call(self.db, &[right_ty, left_ty]) - .return_ty(self.db) - .unwrap_or(Type::Unknown) - }), + (Type::Instance(left_class), Type::Instance(right_class), op) => { + if left_class != right_class && right_ty.is_assignable_to(self.db, left_ty) { + let rhs_reflected = right_class.class_member(self.db, op.reflected_dunder()); + if !rhs_reflected.is_unbound() + && rhs_reflected != left_class.class_member(self.db, op.reflected_dunder()) + { + return rhs_reflected + .call(self.db, &[right_ty, left_ty]) + .return_ty(self.db) + .or_else(|| { + left_class + .class_member(self.db, op.dunder()) + .call(self.db, &[left_ty, right_ty]) + .return_ty(self.db) + }) + .unwrap_or(Type::Unknown); + } + } + left_class + .class_member(self.db, op.dunder()) + .call(self.db, &[left_ty, right_ty]) + .return_ty(self.db) + .or_else(|| { + right_class + .class_member(self.db, op.reflected_dunder()) + .call(self.db, &[right_ty, left_ty]) + .return_ty(self.db) + }) + .unwrap_or(Type::Unknown) + } _ => Type::Todo, // TODO } From 340ebacee9301d2a1f8a6f6b1c3a16afffc6f8c2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 18 Oct 2024 17:43:18 +0100 Subject: [PATCH 05/10] diagnostics! --- .../resources/mdtest/binary/instances.md | 8 +- .../src/types/infer.rs | 97 +++++++++++-------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 29284c1252025..77cd0f8d46b3d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -265,7 +265,7 @@ class A: def __init__(self): self.__add__ = add_impl -# error: [operator] +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`" # revealed: Unknown reveal_type(A() + A()) ``` @@ -275,7 +275,7 @@ reveal_type(A() + A()) ```py class A: pass -# error: [operator] +# error: [unsupported-operator] # revealed: Unknown reveal_type(A() + A()) ``` @@ -293,11 +293,11 @@ class B: class C: pass -# error: [operator] +# error: [unsupported-operator] # revealed: Unknown reveal_type(C() + A()) -# error: [operator] +# error: [unsupported-operator] # revealed: Unknown reveal_type(B() + C()) ``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index a41b58da4aa31..9434fe7cafbb4 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2498,6 +2498,18 @@ impl<'db> TypeInferenceBuilder<'db> { } self.infer_binary_expression_type(left_ty, right_ty, *op) + .unwrap_or_else(|| { + self.add_diagnostic( + binary.into(), + "unsupported-operator", + format_args!( + "Operator `{op}` is unsupported between objects of type `{}` and `{}`", + left_ty.display(self.db), + right_ty.display(self.db) + ), + ); + Type::Unknown + }) } fn infer_binary_expression_type( @@ -2505,72 +2517,78 @@ impl<'db> TypeInferenceBuilder<'db> { left_ty: Type<'db>, right_ty: Type<'db>, op: ast::Operator, - ) -> Type<'db> { + ) -> Option> { match (left_ty, right_ty, op) { // When interacting with Todo, Any and Unknown should propagate (as if we fix this // `Todo` in the future, the result would then become Any or Unknown, respectively.) - (Type::Any, _, _) | (_, Type::Any, _) => Type::Any, - (Type::Unknown, _, _) | (_, Type::Unknown, _) => Type::Unknown, + (Type::Any, _, _) | (_, Type::Any, _) => Some(Type::Any), + (Type::Unknown, _, _) | (_, Type::Unknown, _) => Some(Type::Unknown), - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => n - .checked_add(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( + n.checked_add(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + ), - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Sub) => n - .checked_sub(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Sub) => Some( + n.checked_sub(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + ), - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mult) => n - .checked_mul(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mult) => Some( + n.checked_mul(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + ), (Type::IntLiteral(_), Type::IntLiteral(_), ast::Operator::Div) => { - KnownClass::Float.to_instance(self.db) + Some(KnownClass::Float.to_instance(self.db)) } - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::FloorDiv) => n - .checked_div(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::FloorDiv) => Some( + n.checked_div(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + ), - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mod) => n - .checked_rem(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mod) => Some( + n.checked_rem(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db)), + ), (Type::BytesLiteral(lhs), Type::BytesLiteral(rhs), ast::Operator::Add) => { - Type::BytesLiteral(BytesLiteralType::new( + Some(Type::BytesLiteral(BytesLiteralType::new( self.db, [lhs.value(self.db).as_ref(), rhs.value(self.db).as_ref()] .concat() .into_boxed_slice(), - )) + ))) } (Type::StringLiteral(lhs), Type::StringLiteral(rhs), ast::Operator::Add) => { let lhs_value = lhs.value(self.db).to_string(); let rhs_value = rhs.value(self.db).as_ref(); - if lhs_value.len() + rhs_value.len() <= Self::MAX_STRING_LITERAL_SIZE { + let ty = if lhs_value.len() + rhs_value.len() <= Self::MAX_STRING_LITERAL_SIZE { Type::StringLiteral(StringLiteralType::new(self.db, { (lhs_value + rhs_value).into_boxed_str() })) } else { Type::LiteralString - } + }; + Some(ty) } ( Type::StringLiteral(_) | Type::LiteralString, Type::StringLiteral(_) | Type::LiteralString, ast::Operator::Add, - ) => Type::LiteralString, + ) => Some(Type::LiteralString), (Type::StringLiteral(s), Type::IntLiteral(n), ast::Operator::Mult) | (Type::IntLiteral(n), Type::StringLiteral(s), ast::Operator::Mult) => { - if n < 1 { + let ty = if n < 1 { Type::StringLiteral(StringLiteralType::new(self.db, "")) } else if let Ok(n) = usize::try_from(n) { if n.checked_mul(s.value(self.db).len()) @@ -2586,16 +2604,18 @@ impl<'db> TypeInferenceBuilder<'db> { } } else { Type::LiteralString - } + }; + Some(ty) } (Type::LiteralString, Type::IntLiteral(n), ast::Operator::Mult) | (Type::IntLiteral(n), Type::LiteralString, ast::Operator::Mult) => { - if n < 1 { + let ty = if n < 1 { Type::StringLiteral(StringLiteralType::new(self.db, "")) } else { Type::LiteralString - } + }; + Some(ty) } (Type::Instance(_), Type::IntLiteral(_), op) => { @@ -2644,9 +2664,10 @@ impl<'db> TypeInferenceBuilder<'db> { (Type::Instance(left_class), Type::Instance(right_class), op) => { if left_class != right_class && right_ty.is_assignable_to(self.db, left_ty) { - let rhs_reflected = right_class.class_member(self.db, op.reflected_dunder()); + let reflected_dunder = op.reflected_dunder(); + let rhs_reflected = right_class.class_member(self.db, reflected_dunder); if !rhs_reflected.is_unbound() - && rhs_reflected != left_class.class_member(self.db, op.reflected_dunder()) + && rhs_reflected != left_class.class_member(self.db, reflected_dunder) { return rhs_reflected .call(self.db, &[right_ty, left_ty]) @@ -2656,8 +2677,7 @@ impl<'db> TypeInferenceBuilder<'db> { .class_member(self.db, op.dunder()) .call(self.db, &[left_ty, right_ty]) .return_ty(self.db) - }) - .unwrap_or(Type::Unknown); + }); } } left_class @@ -2670,10 +2690,9 @@ impl<'db> TypeInferenceBuilder<'db> { .call(self.db, &[right_ty, left_ty]) .return_ty(self.db) }) - .unwrap_or(Type::Unknown) } - _ => Type::Todo, // TODO + _ => Some(Type::Todo), // TODO } } From 54b11a4509fdfab1c059d0f267b7c23d56454044 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 18 Oct 2024 19:47:50 +0100 Subject: [PATCH 06/10] review feedback --- .../resources/mdtest/binary/instances.md | 61 +++++++++++++++++-- crates/red_knot_python_semantic/src/types.rs | 27 ++++++++ .../src/types/infer.rs | 13 ++-- 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 77cd0f8d46b3d..2063490bcd0c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -169,7 +169,14 @@ reveal_type(A() + B()) # revealed: str For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but `(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime -(producing `2.2`). +(producing `2.2`). The runtime tries the second one only if the first one +returns `NotImplemented` to signal failure . + +Typeshed and other stubs annotate dunder-method calls that would return +`NotImplemented` as being "illegal" calls. `int.__add__` is annotated as only +"accepting" `int`s, even though it strictly-speaking "accepts" any other object +with raising an error -- it will simply return `NotImplemented`, allowing the +runtime to try the `__radd__` method of the right-hand operand as well. ```py class A: @@ -180,7 +187,10 @@ class B: def __rsub__(self, other: A) -> B: return B() -A() - B() +# TODO: this should be `B` (the return annotation of `B.__rsub__`), +# because `A.__sub__` is annotated as only accepting `A`, +# but `B.__rsub__` will accept `A`. +reveal_type(A() - B()) # revealed: A ``` ## Callable instances as dunders @@ -241,15 +251,58 @@ class A: def __radd__(self, other) -> A: return self reveal_type(A() + 1) # revealed: A +# TODO should be `A` since `int.__add__` doesn't support `A` instances reveal_type(1 + A()) # revealed: int + reveal_type(A() + "foo") # revealed: A -# TODO should be A since `str.__add__` doesn't support A instances +# TODO should be `A` since `str.__add__` doesn't support `A` instances +# TODO overloads reveal_type("foo" + A()) # revealed: @Todo + reveal_type(A() + b"foo") # revealed: A +# TODO should be `A` since `bytes.__add__` doesn't support `A` instances reveal_type(b"foo" + A()) # revealed: bytes + reveal_type(A() + ()) # revealed: A -# TODO this should be A, since tuple's `__add__` doesn't support A instances +# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances reveal_type(() + A()) # revealed: @Todo + +literal_string_instance = "foo" * 1_000_000_000 +# the test is not testing what it's meant to be testing if this isn't a `LiteralString`: +reveal_type(literal_string_instance) # revealed: LiteralString + +reveal_type(A() + literal_string_instance) # revealed: A +# TODO should be `A` since `str.__add__` doesn't support `A` instances +# TODO overloads +reveal_type(literal_string_instance + A()) # revealed: @Todo +``` + +## Operations involving instances of classes inheriting from `Any` + +`Any` and `Unknown` represent a set of possible runtime objects, wherein the +bounds of the set are unknown. Whether the left-hand operand's dunder or the +right-hand operand's reflected dunder depends on whether the right-hand operand +is an instance of a class that is a subclass of the left-hand operand's class +and overrides the reflected dunder. In the following example, because of the +unknowable nature of `Any`/`Unknown`, we must consider both possibilities: +`Any`/`Unknown` might resolve to an unknown third class that inherits from `X` +and overrides `__radd__`; but it also might not. Thus, the correct answer here +for the `reveal_type` is `int | Unknown`. + +```py +from does_not_exist import Foo # error: [unresolved-import] + +reveal_type(Foo) # revealed: Unknown + +class X: + def __add__(self, other: object) -> int: + return 42 + +class Y(Foo): + pass + +# TODO: Should be `int | Unknown`; see above discussion. +reveal_type(X() + Y()) # revealed: int ``` ## Unsupported diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 89b735c4f5aea..0ce75a243e5fd 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1441,6 +1441,32 @@ impl<'db> ClassType<'db> { }) } + pub fn is_subclass_of(self, db: &'db dyn Db, other: Type) -> bool { + match other { + Type::Any | Type::Unknown | Type::Todo => true, + Type::Class(other_class) => { + // TODO: we need to iterate over the *MRO* here, not the bases + (other_class == self) || self.bases(db).any(|base| base == other) + } + // TODO: if a base is a Union, should we return a Vector of `bool`s + // indicating the various possible answers...? + Type::Union(_) | Type::Intersection(_) => false, + // TODO: not sure this is true if it's `Type::Instance(builtins.type)`...? + Type::Instance(_) => false, + Type::Never + | Type::None + | Type::BooleanLiteral(_) + | Type::BytesLiteral(_) + | Type::Function(_) + | Type::IntLiteral(_) + | Type::Module(_) + | Type::LiteralString + | Type::Tuple(_) + | Type::Unbound + | Type::StringLiteral(_) => false, + } + } + /// Returns the class member of this class named `name`. /// /// The member resolves to a member of the class itself or any of its bases. @@ -1671,6 +1697,7 @@ mod tests { #[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))] #[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))] #[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))] + #[test_case(Ty::BuiltinInstance("TypeError"), Ty::BuiltinInstance("Exception"))] fn is_subtype_of(from: Ty, to: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9434fe7cafbb4..97a9229ac6c8b 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2640,15 +2640,12 @@ impl<'db> TypeInferenceBuilder<'db> { op, ), - (Type::Instance(_), Type::StringLiteral(_), op) => { + (Type::Instance(_), Type::StringLiteral(_) | Type::LiteralString, op) => { self.infer_binary_expression_type(left_ty, KnownClass::Str.to_instance(self.db), op) } - (Type::StringLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( - KnownClass::Str.to_instance(self.db), - right_ty, - op, - ), + (Type::StringLiteral(_) | Type::LiteralString, Type::Instance(_), op) => self + .infer_binary_expression_type(KnownClass::Str.to_instance(self.db), right_ty, op), (Type::Instance(_), Type::BytesLiteral(_), op) => self.infer_binary_expression_type( left_ty, @@ -2663,7 +2660,9 @@ impl<'db> TypeInferenceBuilder<'db> { ), (Type::Instance(left_class), Type::Instance(right_class), op) => { - if left_class != right_class && right_ty.is_assignable_to(self.db, left_ty) { + if left_class != right_class + && right_class.is_subclass_of(self.db, Type::Class(left_class)) + { let reflected_dunder = op.reflected_dunder(); let rhs_reflected = right_class.class_member(self.db, reflected_dunder); if !rhs_reflected.is_unbound() From 336556634e08f2451dba68fcd6e46277232bb380 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Oct 2024 15:22:58 -0700 Subject: [PATCH 07/10] simplify is_subclass_of, handle Any/Unknown base --- .../resources/mdtest/binary/instances.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 38 ++++++------------- .../src/types/infer.rs | 4 +- 3 files changed, 13 insertions(+), 31 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 2063490bcd0c6..32b7f83116451 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -302,7 +302,7 @@ class Y(Foo): pass # TODO: Should be `int | Unknown`; see above discussion. -reveal_type(X() + Y()) # revealed: int +reveal_type(X() + Y()) # revealed: Unknown ``` ## Unsupported diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 0ce75a243e5fd..623880878769b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -440,9 +440,9 @@ impl<'db> Type<'db> { .any(|&elem_ty| ty.is_subtype_of(db, elem_ty)), (_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true, (Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false, - (Type::Instance(self_class), Type::Instance(target_class)) => self_class - .bases(db) // TODO: this should iterate through the MRO, not through the bases - .any(|base| base == Type::Class(target_class)), + (Type::Instance(self_class), Type::Instance(target_class)) => { + self_class.is_subclass_of(db, target_class) + } // TODO _ => false, } @@ -1441,30 +1441,14 @@ impl<'db> ClassType<'db> { }) } - pub fn is_subclass_of(self, db: &'db dyn Db, other: Type) -> bool { - match other { - Type::Any | Type::Unknown | Type::Todo => true, - Type::Class(other_class) => { - // TODO: we need to iterate over the *MRO* here, not the bases - (other_class == self) || self.bases(db).any(|base| base == other) - } - // TODO: if a base is a Union, should we return a Vector of `bool`s - // indicating the various possible answers...? - Type::Union(_) | Type::Intersection(_) => false, - // TODO: not sure this is true if it's `Type::Instance(builtins.type)`...? - Type::Instance(_) => false, - Type::Never - | Type::None - | Type::BooleanLiteral(_) - | Type::BytesLiteral(_) - | Type::Function(_) - | Type::IntLiteral(_) - | Type::Module(_) - | Type::LiteralString - | Type::Tuple(_) - | Type::Unbound - | Type::StringLiteral(_) => false, - } + pub fn is_subclass_of(self, db: &'db dyn Db, other: ClassType) -> bool { + // TODO: we need to iterate over the *MRO* here, not the bases + (other == self) + || self.bases(db).any(|base| match base { + Type::Class(base_class) => base_class == other, + Type::Any | Type::Unknown => true, + _ => false, + }) } /// Returns the class member of this class named `name`. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 97a9229ac6c8b..5e785e62c8625 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2660,9 +2660,7 @@ impl<'db> TypeInferenceBuilder<'db> { ), (Type::Instance(left_class), Type::Instance(right_class), op) => { - if left_class != right_class - && right_class.is_subclass_of(self.db, Type::Class(left_class)) - { + if left_class != right_class && right_class.is_subclass_of(self.db, left_class) { let reflected_dunder = op.reflected_dunder(); let rhs_reflected = right_class.class_member(self.db, reflected_dunder); if !rhs_reflected.is_unbound() From 985b235c3a166830710ae71dc0f0a8fca57af374 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Oct 2024 15:32:43 -0700 Subject: [PATCH 08/10] is_subclass_of should not respect Any/Unknown in bases --- .../resources/mdtest/binary/instances.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 32b7f83116451..2063490bcd0c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -302,7 +302,7 @@ class Y(Foo): pass # TODO: Should be `int | Unknown`; see above discussion. -reveal_type(X() + Y()) # revealed: Unknown +reveal_type(X() + Y()) # revealed: int ``` ## Unsupported diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 623880878769b..8b03dec893ec0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1446,7 +1446,9 @@ impl<'db> ClassType<'db> { (other == self) || self.bases(db).any(|base| match base { Type::Class(base_class) => base_class == other, - Type::Any | Type::Unknown => true, + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the + // bases. _ => false, }) } From bc9753b69fdae979c237e78586cdef7623a398cd Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 19 Oct 2024 16:15:58 +0100 Subject: [PATCH 09/10] Apply suggestions from code review --- .../resources/mdtest/binary/instances.md | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 2063490bcd0c6..936ed5fc1c2bf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -82,7 +82,7 @@ reveal_type(B() ^ A()) # revealed: A reveal_type(B() | A()) # revealed: A ``` -## Return different type +## Returning a different type The magic methods aren't required to return the type of `self`: @@ -111,13 +111,14 @@ class A: class B: def __radd__(self, other: A) -> str: return "foo" -# C is a subtype of C, but if the two sides are of equal types, +reveal_type(A() + B()) # revealed: int + +# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types, # the lhs *still* takes precedence class C: def __add__(self, other: C) -> int: return 42 def __radd__(self, other: C) -> str: return "foo" -reveal_type(A() + B()) # revealed: int reveal_type(C() + C()) # revealed: int ``` @@ -170,13 +171,14 @@ reveal_type(A() + B()) # revealed: str For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but `(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime (producing `2.2`). The runtime tries the second one only if the first one -returns `NotImplemented` to signal failure . +returns `NotImplemented` to signal failure. Typeshed and other stubs annotate dunder-method calls that would return `NotImplemented` as being "illegal" calls. `int.__add__` is annotated as only "accepting" `int`s, even though it strictly-speaking "accepts" any other object -with raising an error -- it will simply return `NotImplemented`, allowing the -runtime to try the `__radd__` method of the right-hand operand as well. +without raising an exception -- it will simply return `NotImplemented`, +allowing the runtime to try the `__radd__` method of the right-hand operand +as well. ```py class A: @@ -215,13 +217,13 @@ reveal_type(3j + 3.14) # revealed: complex reveal_type(4.2 + 42) # revealed: float reveal_type(3j + 3) # revealed: complex -# TODO should be complex, need to check arg type and fall back +# TODO should be complex, need to check arg type and fall back to `rhs.__radd__` reveal_type(3.14 + 3j) # revealed: float -# TODO should be float, need to check arg type and fall back +# TODO should be float, need to check arg type and fall back to `rhs.__radd__` reveal_type(42 + 4.2) # revealed: int -# TODO should be complex, need to check arg type and fall back +# TODO should be complex, need to check arg type and fall back to `rhs.__radd__` reveal_type(3 + 3j) # revealed: int def returns_int() -> int: @@ -236,7 +238,7 @@ y = returns_int() reveal_type(x + y) # revealed: int reveal_type(4.2 + x) # revealed: float -# TODO should be float, need to check arg type and fall back +# TODO should be float, need to check arg type and fall back to `rhs.__radd__` reveal_type(y + 4.12) # revealed: int ``` @@ -307,9 +309,9 @@ reveal_type(X() + Y()) # revealed: int ## Unsupported -### Dunder on instance +### Dunder as instance attribute -The magic method must be on the class, not just on the instance: +The magic method must exist on the class, not just on the instance: ```py def add_impl(self, other) -> int: return 1 From a64bf1f86a00fb1d4e965e927e4740a25d64ee64 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 19 Oct 2024 16:18:01 +0100 Subject: [PATCH 10/10] run pre-commit --- .../resources/mdtest/binary/instances.md | 203 +++++++++++++----- 1 file changed, 152 insertions(+), 51 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 936ed5fc1c2bf..fdde6eed2b52e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -14,21 +14,48 @@ We support inference for all Python's binary operators: ```py class A: - def __add__(self, other) -> A: return self - def __sub__(self, other) -> A: return self - def __mul__(self, other) -> A: return self - def __matmul__(self, other) -> A: return self - def __truediv__(self, other) -> A: return self - def __floordiv__(self, other) -> A: return self - def __mod__(self, other) -> A: return self - def __pow__(self, other) -> A: return self - def __lshift__(self, other) -> A: return self - def __rshift__(self, other) -> A: return self - def __and__(self, other) -> A: return self - def __xor__(self, other) -> A: return self - def __or__(self, other) -> A: return self - -class B: pass + def __add__(self, other) -> A: + return self + + def __sub__(self, other) -> A: + return self + + def __mul__(self, other) -> A: + return self + + def __matmul__(self, other) -> A: + return self + + def __truediv__(self, other) -> A: + return self + + def __floordiv__(self, other) -> A: + return self + + def __mod__(self, other) -> A: + return self + + def __pow__(self, other) -> A: + return self + + def __lshift__(self, other) -> A: + return self + + def __rshift__(self, other) -> A: + return self + + def __and__(self, other) -> A: + return self + + def __xor__(self, other) -> A: + return self + + def __or__(self, other) -> A: + return self + + +class B: ... + reveal_type(A() + B()) # revealed: A reveal_type(A() - B()) # revealed: A @@ -51,21 +78,48 @@ We also support inference for reflected operations: ```py class A: - def __radd__(self, other) -> A: return self - def __rsub__(self, other) -> A: return self - def __rmul__(self, other) -> A: return self - def __rmatmul__(self, other) -> A: return self - def __rtruediv__(self, other) -> A: return self - def __rfloordiv__(self, other) -> A: return self - def __rmod__(self, other) -> A: return self - def __rpow__(self, other) -> A: return self - def __rlshift__(self, other) -> A: return self - def __rrshift__(self, other) -> A: return self - def __rand__(self, other) -> A: return self - def __rxor__(self, other) -> A: return self - def __ror__(self, other) -> A: return self - -class B: pass + def __radd__(self, other) -> A: + return self + + def __rsub__(self, other) -> A: + return self + + def __rmul__(self, other) -> A: + return self + + def __rmatmul__(self, other) -> A: + return self + + def __rtruediv__(self, other) -> A: + return self + + def __rfloordiv__(self, other) -> A: + return self + + def __rmod__(self, other) -> A: + return self + + def __rpow__(self, other) -> A: + return self + + def __rlshift__(self, other) -> A: + return self + + def __rrshift__(self, other) -> A: + return self + + def __rand__(self, other) -> A: + return self + + def __rxor__(self, other) -> A: + return self + + def __ror__(self, other) -> A: + return self + + +class B: ... + reveal_type(B() + A()) # revealed: A reveal_type(B() - A()) # revealed: A @@ -88,11 +142,15 @@ The magic methods aren't required to return the type of `self`: ```py class A: - def __add__(self, other) -> int: return 1 - def __rsub__(self, other) -> int: return 1 + def __add__(self, other) -> int: + return 1 + + def __rsub__(self, other) -> int: + return 1 + + +class B: ... -class B: - pass reveal_type(A() + B()) # revealed: int reveal_type(B() - A()) # revealed: int @@ -106,18 +164,27 @@ side, `lhs.__add__` will take precedence: ```py class A: - def __add__(self, other: B) -> int: return 42 + def __add__(self, other: B) -> int: + return 42 + class B: - def __radd__(self, other: A) -> str: return "foo" + def __radd__(self, other: A) -> str: + return "foo" + reveal_type(A() + B()) # revealed: int + # Edge case: C is a subtype of C, *but* if the two sides are of *equal* types, # the lhs *still* takes precedence class C: - def __add__(self, other: C) -> int: return 42 - def __radd__(self, other: C) -> str: return "foo" + def __add__(self, other: C) -> int: + return 42 + + def __radd__(self, other: C) -> str: + return "foo" + reveal_type(C() + C()) # revealed: int ``` @@ -130,19 +197,28 @@ right-hand operand takes precedence. ```py class A: - def __add__(self, other) -> str: return "foo" - def __radd__(self, other) -> str: return "foo" + def __add__(self, other) -> str: + return "foo" + + def __radd__(self, other) -> str: + return "foo" + class MyString(str): ... + class B(A): - def __radd__(self, other) -> MyString: return MyString() + def __radd__(self, other) -> MyString: + return MyString() + reveal_type(A() + B()) # revealed: MyString + # N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__` class C(B): ... + # TODO: we currently only understand direct subclasses as subtypes of the superclass. # We need to iterate through the full MRO rather than just the class's bases; # if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being @@ -158,10 +234,15 @@ still takes precedence: ```py class A: - def __add__(self, other) -> str: return "foo" - def __radd__(self, other) -> int: return 42 + def __add__(self, other) -> str: + return "foo" + + def __radd__(self, other) -> int: + return 42 + + +class B(A): ... -class B(A): pass reveal_type(A() + B()) # revealed: str ``` @@ -185,10 +266,12 @@ class A: def __sub__(self, other: A) -> A: return A() + class B: def __rsub__(self, other: A) -> B: return B() + # TODO: this should be `B` (the return annotation of `B.__rsub__`), # because `A.__sub__` is annotated as only accepting `A`, # but `B.__rsub__` will accept `A`. @@ -204,9 +287,11 @@ class A: def __call__(self, other) -> int: return 42 + class B: __add__ = A() + reveal_type(B() + B()) # revealed: int ``` @@ -226,12 +311,15 @@ reveal_type(42 + 4.2) # revealed: int # TODO should be complex, need to check arg type and fall back to `rhs.__radd__` reveal_type(3 + 3j) # revealed: int + def returns_int() -> int: return 42 + def returns_bool() -> bool: return True + x = returns_bool() y = returns_int() @@ -249,8 +337,12 @@ instance handling for its instance super-type. ```py class A: - def __add__(self, other) -> A: return self - def __radd__(self, other) -> A: return self + def __add__(self, other) -> A: + return self + + def __radd__(self, other) -> A: + return self + reveal_type(A() + 1) # revealed: A # TODO should be `A` since `int.__add__` doesn't support `A` instances @@ -296,12 +388,14 @@ from does_not_exist import Foo # error: [unresolved-import] reveal_type(Foo) # revealed: Unknown + class X: def __add__(self, other: object) -> int: return 42 -class Y(Foo): - pass + +class Y(Foo): ... + # TODO: Should be `int | Unknown`; see above discussion. reveal_type(X() + Y()) # revealed: int @@ -314,12 +408,15 @@ reveal_type(X() + Y()) # revealed: int The magic method must exist on the class, not just on the instance: ```py -def add_impl(self, other) -> int: return 1 +def add_impl(self, other) -> int: + return 1 + class A: def __init__(self): self.__add__ = add_impl + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`" # revealed: Unknown reveal_type(A() + A()) @@ -328,7 +425,8 @@ reveal_type(A() + A()) ### Missing dunder ```py -class A: pass +class A: ... + # error: [unsupported-operator] # revealed: Unknown @@ -343,10 +441,13 @@ A left-hand dunder method doesn't apply for the right-hand operand, or vice vers class A: def __add__(self, other) -> int: ... + class B: def __radd__(self, other) -> int: ... -class C: pass + +class C: ... + # error: [unsupported-operator] # revealed: Unknown