Skip to content

Commit c91b457

Browse files
authored
[ty] Introduce TypeRelation::Redundancy (#20602)
## Summary The union `T | U` can be validly simplified to `U` iff: 1. `T` is a subtype of `U` OR 2. `T` is equivalent to `U` OR 3. `U` is a union and contains a type that is equivalent to `T` OR 4. `T` is an intersection and contains a type that is equivalent to `U` (In practice, the only situation in which 2, 3 or 4 would be true when (1) was not true would be if `T` or `U` is a dynamic type.) Currently we achieve these simplifications in the union builder by doing something along the lines of `t.is_subtype_of(db, u) || t.is_equivalent_to_(db, u) || t.into_intersection().is_some_and(|intersection| intersection.positive(db).contains(&u)) || u.into_union().is_some_and(|union| union.elements(db).contains(&t))`. But this is both slow and misses some cases (it doesn't simplify the union `Any | (Unknown & ~None)` to `Any`, for example). We can improve the consistency and performance of our union simplifications by adding a third type relation that sits in between `TypeRelation::Subtyping` and `TypeRelation::Assignability`: `TypeRelation::UnionSimplification`. This change leads to simpler, more user-friendly types due to the more consistent simplification. It also lead to a pretty huge performance improvement! ## Test Plan Existing tests, plus some new ones.
1 parent 673167a commit c91b457

File tree

9 files changed

+281
-92
lines changed

9 files changed

+281
-92
lines changed

crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ static_assert(is_equivalent_to(Any, Any | Intersection[Any, str]))
138138
static_assert(is_equivalent_to(Any, Intersection[str, Any] | Any))
139139
static_assert(is_equivalent_to(Any, Any | Intersection[Any, Not[None]]))
140140
static_assert(is_equivalent_to(Any, Intersection[Not[None], Any] | Any))
141+
142+
static_assert(is_equivalent_to(Any, Unknown | Intersection[Unknown, str]))
143+
static_assert(is_equivalent_to(Any, Intersection[str, Unknown] | Unknown))
144+
static_assert(is_equivalent_to(Any, Unknown | Intersection[Unknown, Not[None]]))
145+
static_assert(is_equivalent_to(Any, Intersection[Not[None], Unknown] | Unknown))
141146
```
142147

143148
## Tuples

crates/ty_python_semantic/resources/mdtest/union_types.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,74 @@ def _(c: BC, d: BD):
306306
reveal_type(c) # revealed: Literal[b""]
307307
reveal_type(d) # revealed: Literal[b""]
308308
```
309+
310+
## Unions of tuples
311+
312+
A union of a fixed-length tuple and a variable-length tuple must be collapsed to the variable-length
313+
element, never to the fixed-length element (`tuple[()] | tuple[Any, ...]` -> `tuple[Any, ...]`, not
314+
`tuple[()]`).
315+
316+
```py
317+
from typing import Any
318+
319+
def f(
320+
a: tuple[()] | tuple[int, ...],
321+
b: tuple[int, ...] | tuple[()],
322+
c: tuple[int] | tuple[str, ...],
323+
d: tuple[str, ...] | tuple[int],
324+
e: tuple[()] | tuple[Any, ...],
325+
f: tuple[Any, ...] | tuple[()],
326+
g: tuple[Any, ...] | tuple[Any | str, ...],
327+
h: tuple[Any | str, ...] | tuple[Any, ...],
328+
):
329+
reveal_type(a) # revealed: tuple[int, ...]
330+
reveal_type(b) # revealed: tuple[int, ...]
331+
reveal_type(c) # revealed: tuple[int] | tuple[str, ...]
332+
reveal_type(d) # revealed: tuple[str, ...] | tuple[int]
333+
reveal_type(e) # revealed: tuple[Any, ...]
334+
reveal_type(f) # revealed: tuple[Any, ...]
335+
reveal_type(g) # revealed: tuple[Any | str, ...]
336+
reveal_type(h) # revealed: tuple[Any | str, ...]
337+
```
338+
339+
## Unions of other generic containers
340+
341+
```toml
342+
[environment]
343+
python-version = "3.12"
344+
```
345+
346+
```py
347+
from typing import Any
348+
349+
class Bivariant[T]: ...
350+
351+
class Covariant[T]:
352+
def get(self) -> T:
353+
raise NotImplementedError
354+
355+
class Contravariant[T]:
356+
def receive(self, input: T) -> None: ...
357+
358+
class Invariant[T]:
359+
mutable_attribute: T
360+
361+
def _(
362+
a: Bivariant[Any] | Bivariant[Any | str],
363+
b: Bivariant[Any | str] | Bivariant[Any],
364+
c: Covariant[Any] | Covariant[Any | str],
365+
d: Covariant[Any | str] | Covariant[Any],
366+
e: Contravariant[Any | str] | Contravariant[Any],
367+
f: Contravariant[Any] | Contravariant[Any | str],
368+
g: Invariant[Any] | Invariant[Any | str],
369+
h: Invariant[Any | str] | Invariant[Any],
370+
):
371+
reveal_type(a) # revealed: Bivariant[Any]
372+
reveal_type(b) # revealed: Bivariant[Any | str]
373+
reveal_type(c) # revealed: Covariant[Any | str]
374+
reveal_type(d) # revealed: Covariant[Any | str]
375+
reveal_type(e) # revealed: Contravariant[Any]
376+
reveal_type(f) # revealed: Contravariant[Any]
377+
reveal_type(g) # revealed: Invariant[Any] | Invariant[Any | str]
378+
reveal_type(h) # revealed: Invariant[Any | str] | Invariant[Any]
379+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 172 additions & 53 deletions
Large diffs are not rendered by default.

crates/ty_python_semantic/src/types/builder.rs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -502,18 +502,9 @@ impl<'db> UnionBuilder<'db> {
502502
}
503503

504504
if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) {
505-
if ty.is_equivalent_to(self.db, element_type)
506-
|| ty.is_subtype_of(self.db, element_type)
507-
|| ty.into_intersection().is_some_and(|intersection| {
508-
intersection.positive(self.db).contains(&element_type)
509-
})
510-
{
505+
if ty.is_redundant_with(self.db, element_type) {
511506
return;
512-
} else if element_type.is_subtype_of(self.db, ty)
513-
|| element_type
514-
.into_intersection()
515-
.is_some_and(|intersection| intersection.positive(self.db).contains(&ty))
516-
{
507+
} else if element_type.is_redundant_with(self.db, ty) {
517508
to_remove.push(index);
518509
} else if ty_negated.is_subtype_of(self.db, element_type) {
519510
// We add `ty` to the union. We just checked that `~ty` is a subtype of an
@@ -930,13 +921,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
930921
let mut to_remove = SmallVec::<[usize; 1]>::new();
931922
for (index, existing_positive) in self.positive.iter().enumerate() {
932923
// S & T = S if S <: T
933-
if existing_positive.is_subtype_of(db, new_positive)
934-
|| existing_positive.is_equivalent_to(db, new_positive)
935-
{
924+
if existing_positive.is_redundant_with(db, new_positive) {
936925
return;
937926
}
938927
// same rule, reverse order
939-
if new_positive.is_subtype_of(db, *existing_positive) {
928+
if new_positive.is_redundant_with(db, *existing_positive) {
940929
to_remove.push(index);
941930
}
942931
// A & B = Never if A and B are disjoint
@@ -1027,9 +1016,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
10271016
let mut to_remove = SmallVec::<[usize; 1]>::new();
10281017
for (index, existing_negative) in self.negative.iter().enumerate() {
10291018
// ~S & ~T = ~T if S <: T
1030-
if existing_negative.is_subtype_of(db, new_negative)
1031-
|| existing_negative.is_equivalent_to(db, new_negative)
1032-
{
1019+
if existing_negative.is_redundant_with(db, new_negative) {
10331020
to_remove.push(index);
10341021
}
10351022
// same rule, reverse order

crates/ty_python_semantic/src/types/class.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,9 @@ impl<'db> ClassType<'db> {
551551
self.iter_mro(db).when_any(db, |base| {
552552
match base {
553553
ClassBase::Dynamic(_) => match relation {
554-
TypeRelation::Subtyping => ConstraintSet::from(other.is_object(db)),
554+
TypeRelation::Subtyping | TypeRelation::Redundancy => {
555+
ConstraintSet::from(other.is_object(db))
556+
}
555557
TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)),
556558
},
557559

crates/ty_python_semantic/src/types/function.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,9 @@ impl<'db> FunctionType<'db> {
969969
_visitor: &HasRelationToVisitor<'db>,
970970
) -> ConstraintSet<'db> {
971971
match relation {
972-
TypeRelation::Subtyping => ConstraintSet::from(self.is_subtype_of(db, other)),
972+
TypeRelation::Subtyping | TypeRelation::Redundancy => {
973+
ConstraintSet::from(self.is_subtype_of(db, other))
974+
}
973975
TypeRelation::Assignability => ConstraintSet::from(self.is_assignable_to(db, other)),
974976
}
975977
}

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -620,22 +620,26 @@ fn has_relation_in_invariant_position<'db>(
620620
base_type.has_relation_to_impl(db, *derived_type, relation, visitor)
621621
}),
622622
// For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B]
623-
(None, Some(base_mat), TypeRelation::Subtyping) => is_subtype_in_invariant_position(
624-
db,
625-
derived_type,
626-
MaterializationKind::Top,
627-
base_type,
628-
base_mat,
629-
visitor,
630-
),
631-
(Some(derived_mat), None, TypeRelation::Subtyping) => is_subtype_in_invariant_position(
632-
db,
633-
derived_type,
634-
derived_mat,
635-
base_type,
636-
MaterializationKind::Bottom,
637-
visitor,
638-
),
623+
(None, Some(base_mat), TypeRelation::Subtyping | TypeRelation::Redundancy) => {
624+
is_subtype_in_invariant_position(
625+
db,
626+
derived_type,
627+
MaterializationKind::Top,
628+
base_type,
629+
base_mat,
630+
visitor,
631+
)
632+
}
633+
(Some(derived_mat), None, TypeRelation::Subtyping | TypeRelation::Redundancy) => {
634+
is_subtype_in_invariant_position(
635+
db,
636+
derived_type,
637+
derived_mat,
638+
base_type,
639+
MaterializationKind::Bottom,
640+
visitor,
641+
)
642+
}
639643
// And A <~ B (assignability) is Bottom[A] <: Top[B]
640644
(None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position(
641645
db,

crates/ty_python_semantic/src/types/subclass_of.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ impl<'db> SubclassOfType<'db> {
138138
) -> ConstraintSet<'db> {
139139
match (self.subclass_of, other.subclass_of) {
140140
(SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => {
141-
ConstraintSet::from(relation.is_assignability())
141+
ConstraintSet::from(!relation.is_subtyping())
142142
}
143143
(SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => {
144144
ConstraintSet::from(other_class.is_object(db) || relation.is_assignability())

crates/ty_python_semantic/src/types/tuple.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,8 +757,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
757757
// (or any other dynamic type), then the `...` is the _gradual choice_ of all
758758
// possible lengths. This means that `tuple[Any, ...]` can match any tuple of any
759759
// length.
760-
if relation == TypeRelation::Subtyping || !matches!(self.variable, Type::Dynamic(_))
761-
{
760+
if !relation.is_assignability() || !self.variable.is_dynamic() {
762761
return ConstraintSet::from(false);
763762
}
764763

0 commit comments

Comments
 (0)