From 55bccf66806db3bacfa64d11567fe381b64beab9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 19 Oct 2024 19:09:54 +0100 Subject: [PATCH] [red-knot] Fix edge case for binary-expression inference where the lhs and rhs are the exact same type (#13823) ## Summary This fixes an edge case that @carljm and I missed when implementing https://github.com/astral-sh/ruff/pull/13800. Namely, if the left-hand operand is the _exact same type_ as the right-hand operand, the reflected dunder on the right-hand operand is never tried: ```pycon >>> class Foo: ... def __radd__(self, other): ... return 42 ... >>> Foo() + Foo() Traceback (most recent call last): File "", line 1, in Foo() + Foo() ~~~~~~^~~~~~~ TypeError: unsupported operand type(s) for +: 'Foo' and 'Foo' ``` This edge case _is_ covered in Brett's blog at https://snarky.ca/unravelling-binary-arithmetic-operations-in-python/, but I missed it amongst all the other subtleties of this algorithm. The motivations and history behind it were discussed in https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/ ## Test Plan I added an mdtest for this cornercase. --- .../resources/mdtest/binary/instances.md | 19 +++++++++++++++++++ .../src/types/infer.rs | 12 ++++++++---- 2 files changed, 27 insertions(+), 4 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 fdde6eed2b..ba2b9c5762 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -458,6 +458,25 @@ reveal_type(C() + A()) reveal_type(B() + C()) ``` +### Reflected dunder is not tried between two objects of the same type + +For the specific case where the left-hand operand is the exact same type as the +right-hand operand, the reflected dunder of the right-hand operand is not +tried; the runtime short-circuits after trying the unreflected dunder of the +left-hand operand. For context, see +[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/). + +```py +class Foo: + def __radd__(self, other: Foo) -> Foo: + return self + + +# error: [unsupported-operator] +# revealed: Unknown +reveal_type(Foo() + Foo()) +``` + ### Wrong type TODO: check signature and error if `other` is the wrong type diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 5e785e62c8..3e0edef49b 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2682,10 +2682,14 @@ impl<'db> TypeInferenceBuilder<'db> { .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) + if left_class == right_class { + None + } else { + right_class + .class_member(self.db, op.reflected_dunder()) + .call(self.db, &[right_ty, left_ty]) + .return_ty(self.db) + } }) }