Skip to content

Commit 3ef117e

Browse files
committed
[ty] Fix simplification of T & ~T for non-fully-static types
1 parent f9688bd commit 3ef117e

File tree

2 files changed

+60
-0
lines changed

2 files changed

+60
-0
lines changed

crates/ty_python_semantic/resources/mdtest/intersection_types.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,11 @@ def unknown(
907907
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
908908
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
909909

910+
```toml
911+
[environment]
912+
python-version = "3.12"
913+
```
914+
910915
```py
911916
from typing import Any
912917
from ty_extensions import Intersection, Not, Unknown
@@ -924,6 +929,50 @@ def unknown(
924929
) -> None:
925930
reveal_type(i1) # revealed: Unknown
926931
reveal_type(i2) # revealed: Unknown
932+
933+
class Covariant[T]:
934+
def get(self) -> T:
935+
raise NotImplementedError
936+
937+
def covariant(
938+
i1: Intersection[Covariant[Any], Not[Covariant[Any]]],
939+
i2: Intersection[Not[Covariant[Any]], Covariant[Any]],
940+
) -> None:
941+
reveal_type(i1) # revealed: Covariant[Any]
942+
reveal_type(i2) # revealed: Covariant[Any]
943+
944+
class Contravariant[T]:
945+
def receive(self, input: T): ...
946+
947+
def contravariant(
948+
i1: Intersection[Contravariant[Any], Not[Contravariant[Any]]],
949+
i2: Intersection[Not[Contravariant[Any]], Contravariant[Any]],
950+
) -> None:
951+
reveal_type(i1) # revealed: Contravariant[Any]
952+
reveal_type(i2) # revealed: Contravariant[Any]
953+
954+
class Invariant[T]:
955+
mutable_attribute: T
956+
957+
def invariant(
958+
i1: Intersection[Invariant[Any], Not[Invariant[Any]]],
959+
i2: Intersection[Not[Invariant[Any]], Invariant[Any]],
960+
) -> None:
961+
reveal_type(i1) # revealed: Invariant[Any]
962+
reveal_type(i2) # revealed: Invariant[Any]
963+
964+
class Bivariant[T]: ...
965+
966+
# Because of bivariance, the specialisation here is meaningless;
967+
# `Bivariant[Any]` is arguably still a fully static type, even
968+
# though it is specialised with a gradual form! Thus self-cancellation
969+
# here is fine:
970+
def bivariant(
971+
i1: Intersection[Bivariant[Any], Not[Bivariant[Any]]],
972+
i2: Intersection[Not[Bivariant[Any]], Bivariant[Any]],
973+
) -> None:
974+
reveal_type(i1) # revealed: Never
975+
reveal_type(i2) # revealed: Never
927976
```
928977

929978
### Mixed dynamic types

crates/ty_python_semantic/src/types/builder.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,12 @@ impl<'db> InnerIntersectionBuilder<'db> {
958958
self.positive.insert(Type::Never);
959959
return;
960960
}
961+
// `T & ~T` = `T` if `T` is not a subtype of `T`
962+
// (this can only be true if `T` is a non-fully-static type)
963+
if existing_negative.is_equivalent_to(db, new_positive) {
964+
to_remove.push(index);
965+
continue;
966+
}
961967
// A & ~B = A if A and B are disjoint
962968
if existing_negative.is_disjoint_from(db, new_positive) {
963969
to_remove.push(index);
@@ -1048,6 +1054,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
10481054
self.positive.insert(Type::Never);
10491055
return;
10501056
}
1057+
// `T & ~T` = `T` if `T` is not a subtype of `T`
1058+
// (this can only be true if `T` is a non-fully-static type)
1059+
if existing_positive.is_equivalent_to(db, new_negative) {
1060+
return;
1061+
}
10511062
// A & ~B = A if A and B are disjoint
10521063
if existing_positive.is_disjoint_from(db, new_negative) {
10531064
return;

0 commit comments

Comments
 (0)