Skip to content

Commit

Permalink
[red-knot] Fix edge case for binary-expression inference where the lh…
Browse files Browse the repository at this point in the history
…s and rhs are the exact same type (#13823)

## Summary

This fixes an edge case that @carljm and I missed when implementing
#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 "<python-input-1>", line 1, in <module>
    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.
  • Loading branch information
AlexWaygood authored Oct 19, 2024
1 parent f4b5e70 commit 55bccf6
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 8 additions & 4 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}

Expand Down

0 comments on commit 55bccf6

Please sign in to comment.