[red-knot] Simplify unions of T and ~T (#15400)

## Summary

Simplify unions of `T` and `~T` to `object`.

## Test Plan

Adapted existing tests.
This commit is contained in:
David Peter 2025-01-10 23:00:52 +01:00 committed by GitHub
parent 398f2e8b0c
commit 2d82445794
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 15 deletions

View file

@ -313,22 +313,24 @@ def _(
### Union of a type and its negation ### Union of a type and its negation
Similarly, if we have both `P` and `~P` in a _union_, we could simplify that to `object`. However, Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`.
this is a rather costly operation which would require us to build the negation of each type that we
add to a union, so this is not implemented at the moment.
```py ```py
from knot_extensions import Intersection, Not from knot_extensions import Intersection, Not
class P: ... class P: ...
class Q: ...
def _( def _(
i1: P | Not[P], i1: P | Not[P],
i2: Not[P] | P, i2: Not[P] | P,
i3: P | Q | Not[P],
i4: Not[P] | Q | P,
) -> None: ) -> None:
# These could be simplified to `object` reveal_type(i1) # revealed: object
reveal_type(i1) # revealed: P | ~P reveal_type(i2) # revealed: object
reveal_type(i2) # revealed: ~P | P reveal_type(i3) # revealed: object
reveal_type(i4) # revealed: object
``` ```
### Negation is an involution ### Negation is an involution

View file

@ -21,22 +21,22 @@ else:
if x and not x: if x and not x:
reveal_type(x) # revealed: Never reveal_type(x) # revealed: Never
else: else:
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()] reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
if not (x and not x): if not (x and not x):
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()] reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
else: else:
reveal_type(x) # revealed: Never reveal_type(x) # revealed: Never
if x or not x: if x or not x:
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()] reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
else: else:
reveal_type(x) # revealed: Never reveal_type(x) # revealed: Never
if not (x or not x): if not (x or not x):
reveal_type(x) # revealed: Never reveal_type(x) # revealed: Never
else: else:
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()] reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
if (isinstance(x, int) or isinstance(x, str)) and x: if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1, True, "foo"] reveal_type(x) # revealed: Literal[-1, True, "foo"]
@ -87,10 +87,10 @@ def f(x: A | B):
if x and not x: if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
else: else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy reveal_type(x) # revealed: A | B
if x or not x: if x or not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy reveal_type(x) # revealed: A | B
else: else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
``` ```
@ -214,10 +214,9 @@ if x and not x:
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
else: else:
y = x y = x
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy reveal_type(y) # revealed: A
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023) reveal_type(y) # revealed: A
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
``` ```
## Truthiness of classes ## Truthiness of classes

View file

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