Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/binary/unions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Binary operations on union types

Binary operations on union types are only available if they are supported for all possible
combinations of types:

```py
def f1(i: int, u: int | None):
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int` and `int | None`"
reveal_type(i + u) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | None` and `int`"
reveal_type(u + i) # revealed: Unknown
```

`int` can be added to `int`, and `str` can be added to `str`, but expressions of type `int | str`
cannot be added, because that would require addition of `int` and `str` or vice versa:

```py
def f2(i: int, s: str, int_or_str: int | str):
i + i
s + s
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
reveal_type(int_or_str + int_or_str) # revealed: Unknown
```

However, if an operation is supported for all possible combinations, the result will be a union of
the possible outcomes:

```py
from typing import Literal

def f3(two_or_three: Literal[2, 3], a_or_b: Literal["a", "b"]):
reveal_type(two_or_three + two_or_three) # revealed: Literal[4, 5, 6]
reveal_type(two_or_three**two_or_three) # revealed: Literal[4, 8, 9, 27]

reveal_type(a_or_b + a_or_b) # revealed: Literal["aa", "ab", "ba", "bb"]

reveal_type(two_or_three * a_or_b) # revealed: Literal["aa", "bb", "aaa", "bbb"]
```

We treat a type annotation of `float` as a union of `int` and `float`, so union handling is relevant
here:

```py
def f4(x: float, y: float):
reveal_type(x + y) # revealed: int | float
reveal_type(x - y) # revealed: int | float
reveal_type(x * y) # revealed: int | float
reveal_type(x / y) # revealed: int | float
reveal_type(x // y) # revealed: int | float
reveal_type(x % y) # revealed: int | float
```
19 changes: 17 additions & 2 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4048,6 +4048,23 @@ impl<'db> TypeInferenceBuilder<'db> {
op: ast::Operator,
) -> Option<Type<'db>> {
match (left_ty, right_ty, op) {
(Type::Union(lhs_union), rhs, _) => {
let mut union = UnionBuilder::new(self.db());
for lhs in lhs_union.elements(self.db()) {
let result = self.infer_binary_expression_type(*lhs, rhs, op)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of short-circuiting here, we could probably also build a better diagnostic, telling you which exact element-combination didn't work. But it seems low priority and doesn't necessarily have to be included here, I think.

union = union.add(result);
}
Some(union.build())
}
(lhs, Type::Union(rhs_union), _) => {
let mut union = UnionBuilder::new(self.db());
for rhs in rhs_union.elements(self.db()) {
let result = self.infer_binary_expression_type(lhs, *rhs, op)?;
union = union.add(result);
}
Some(union.build())
}

// Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future,
// the result would then become Any or Unknown, respectively).
(any @ Type::Dynamic(DynamicType::Any), _, _)
Expand Down Expand Up @@ -4179,7 +4196,6 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
Expand All @@ -4196,7 +4212,6 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
Expand Down
Loading