Skip to content

Commit 2d82445

Browse files
authored
[red-knot] Simplify unions of T and ~T (#15400)
## Summary Simplify unions of `T` and `~T` to `object`. ## Test Plan Adapted existing tests.
1 parent 398f2e8 commit 2d82445

File tree

3 files changed

+29
-15
lines changed

3 files changed

+29
-15
lines changed

crates/red_knot_python_semantic/resources/mdtest/intersection_types.md

+8-6
Original file line numberDiff line numberDiff line change
@@ -313,22 +313,24 @@ def _(
313313

314314
### Union of a type and its negation
315315

316-
Similarly, if we have both `P` and `~P` in a _union_, we could simplify that to `object`. However,
317-
this is a rather costly operation which would require us to build the negation of each type that we
318-
add to a union, so this is not implemented at the moment.
316+
Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`.
319317

320318
```py
321319
from knot_extensions import Intersection, Not
322320

323321
class P: ...
322+
class Q: ...
324323

325324
def _(
326325
i1: P | Not[P],
327326
i2: Not[P] | P,
327+
i3: P | Q | Not[P],
328+
i4: Not[P] | Q | P,
328329
) -> None:
329-
# These could be simplified to `object`
330-
reveal_type(i1) # revealed: P | ~P
331-
reveal_type(i2) # revealed: ~P | P
330+
reveal_type(i1) # revealed: object
331+
reveal_type(i2) # revealed: object
332+
reveal_type(i3) # revealed: object
333+
reveal_type(i4) # revealed: object
332334
```
333335

334336
### Negation is an involution

crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md

+8-9
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@ else:
2121
if x and not x:
2222
reveal_type(x) # revealed: Never
2323
else:
24-
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()]
24+
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
2525

2626
if not (x and not x):
27-
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()]
27+
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
2828
else:
2929
reveal_type(x) # revealed: Never
3030

3131
if x or not x:
32-
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()]
32+
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
3333
else:
3434
reveal_type(x) # revealed: Never
3535

3636
if not (x or not x):
3737
reveal_type(x) # revealed: Never
3838
else:
39-
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()]
39+
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
4040

4141
if (isinstance(x, int) or isinstance(x, str)) and x:
4242
reveal_type(x) # revealed: Literal[-1, True, "foo"]
@@ -87,10 +87,10 @@ def f(x: A | B):
8787
if x and not x:
8888
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
8989
else:
90-
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
90+
reveal_type(x) # revealed: A | B
9191

9292
if x or not x:
93-
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
93+
reveal_type(x) # revealed: A | B
9494
else:
9595
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
9696
```
@@ -214,10 +214,9 @@ if x and not x:
214214
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
215215
else:
216216
y = x
217-
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
217+
reveal_type(y) # revealed: A
218218

219-
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
220-
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
219+
reveal_type(y) # revealed: A
221220
```
222221

223222
## Truthiness of classes

crates/red_knot_python_semantic/src/types/builder.rs

+13
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ impl<'db> UnionBuilder<'db> {
6565

6666
let mut to_add = ty;
6767
let mut to_remove = SmallVec::<[usize; 2]>::new();
68+
let ty_negated = ty.negate(self.db);
69+
6870
for (index, element) in self.elements.iter().enumerate() {
6971
if Some(*element) == bool_pair {
7072
to_add = KnownClass::Bool.to_instance(self.db);
@@ -80,6 +82,17 @@ impl<'db> UnionBuilder<'db> {
8082
return self;
8183
} else if element.is_subtype_of(self.db, ty) {
8284
to_remove.push(index);
85+
} else if ty_negated.is_subtype_of(self.db, *element) {
86+
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
87+
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
88+
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
89+
// just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that
90+
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
91+
// the whole union to just `object`, since all other potential elements would also be subtypes of
92+
// `object`.
93+
self.elements.clear();
94+
self.elements.push(KnownClass::Object.to_instance(self.db));
95+
return self;
8396
}
8497
}
8598
match to_remove[..] {

0 commit comments

Comments
 (0)