-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[red-knot] WIP: binary arithmetic on instances
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
- Loading branch information
1 parent
5c537b6
commit eeae718
Showing
4 changed files
with
367 additions
and
2 deletions.
There are no files selected for viewing
264 changes: 264 additions & 0 deletions
264
crates/red_knot_python_semantic/resources/mdtest/binary/instances.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
# Binary operations on instances | ||
|
||
Binary operations in Python are implemented by means of magic double-underscore methods. | ||
|
||
For references, see: | ||
|
||
- <https://snarky.ca/unravelling-binary-arithmetic-operations-in-python/> | ||
- <https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types> | ||
|
||
## 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters