diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7b643a168a..1ce97e93c6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1050,13 +1050,6 @@ impl<'db> Type<'db> { } } - pub(crate) const fn into_intersection(self) -> Option> { - match self { - Type::Intersection(intersection_type) => Some(intersection_type), - _ => None, - } - } - #[cfg(test)] #[track_caller] pub(crate) fn expect_union(self) -> UnionType<'db> { @@ -1471,6 +1464,11 @@ impl<'db> Type<'db> { self.has_relation_to(db, target, TypeRelation::Assignability) } + pub(crate) fn is_redundant_in_union_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { + self.has_relation_to(db, other, TypeRelation::UnionSimplification) + .is_always_satisfied() + } + fn has_relation_to( self, db: &'db dyn Db, @@ -1492,7 +1490,7 @@ impl<'db> Type<'db> { // // Note that we could do a full equivalence check here, but that would be both expensive // and unnecessary. This early return is only an optimisation. - if (relation.is_assignability() || self.subtyping_is_always_reflexive()) && self == target { + if (!relation.is_subtyping() || self.subtyping_is_always_reflexive()) && self == target { return ConstraintSet::from(true); } @@ -1509,9 +1507,10 @@ impl<'db> Type<'db> { // It is a subtype of all other types. (Type::Never, _) => ConstraintSet::from(true), - // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were - // handled above. It's always assignable, though. - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { + // In some specific situations, `Any` can be simplified out of unions and intersections, + // but this is not true for divergent types. + (Type::Dynamic(DynamicType::Divergent(_)), _) + | (_, Type::Dynamic(DynamicType::Divergent(_))) => { ConstraintSet::from(relation.is_assignability()) } @@ -1536,10 +1535,27 @@ impl<'db> Type<'db> { .has_relation_to_impl(db, right, relation, visitor) } - (Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => { - // TODO: Implement assignability and subtyping for TypedDict - ConstraintSet::from(relation.is_assignability()) - } + (Type::Dynamic(_), _) => ConstraintSet::from(match relation { + TypeRelation::Subtyping => false, + TypeRelation::Assignability => true, + TypeRelation::UnionSimplification => match target { + Type::Dynamic(_) => true, + Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), + _ => false, + }, + }), + + (_, Type::Dynamic(_)) => ConstraintSet::from(match relation { + TypeRelation::Subtyping => false, + TypeRelation::Assignability => true, + TypeRelation::UnionSimplification => match self { + Type::Dynamic(_) => true, + Type::Intersection(intersection) => { + intersection.positive(db).iter().any(Type::is_dynamic) + } + _ => false, + }, + }), // In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied: // 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`. @@ -1687,6 +1703,11 @@ impl<'db> Type<'db> { // TODO: Infer specializations here (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => ConstraintSet::from(false), + (Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => { + // TODO: Implement assignability and subtyping for TypedDict + ConstraintSet::from(relation.is_assignability()) + } + // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. (left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()), @@ -9098,6 +9119,7 @@ impl<'db> ConstructorCallError<'db> { #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub(crate) enum TypeRelation { Subtyping, + UnionSimplification, Assignability, } @@ -9105,6 +9127,10 @@ impl TypeRelation { pub(crate) const fn is_assignability(self) -> bool { matches!(self, TypeRelation::Assignability) } + + pub(crate) const fn is_subtyping(self) -> bool { + matches!(self, TypeRelation::Subtyping) + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)] diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 556edcb368..0e2fbc013f 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -502,18 +502,9 @@ impl<'db> UnionBuilder<'db> { } if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) { - if ty.is_equivalent_to(self.db, element_type) - || ty.is_subtype_of(self.db, element_type) - || ty.into_intersection().is_some_and(|intersection| { - intersection.positive(self.db).contains(&element_type) - }) - { + if ty.is_redundant_in_union_with(self.db, element_type) { return; - } else if element_type.is_subtype_of(self.db, ty) - || element_type - .into_intersection() - .is_some_and(|intersection| intersection.positive(self.db).contains(&ty)) - { + } else if element_type.is_redundant_in_union_with(self.db, ty) { to_remove.push(index); } else if ty_negated.is_subtype_of(self.db, element_type) { // We add `ty` to the union. We just checked that `~ty` is a subtype of an @@ -930,13 +921,11 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T - if existing_positive.is_subtype_of(db, new_positive) - || existing_positive.is_equivalent_to(db, new_positive) - { + if existing_positive.is_redundant_in_union_with(db, new_positive) { return; } // same rule, reverse order - if new_positive.is_subtype_of(db, *existing_positive) { + if new_positive.is_redundant_in_union_with(db, *existing_positive) { to_remove.push(index); } // A & B = Never if A and B are disjoint @@ -953,7 +942,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { // S & ~T = Never if S <: T - if new_positive.is_subtype_of(db, *existing_negative) { + if new_positive.is_redundant_in_union_with(db, *existing_negative) { *self = Self::default(); self.positive.insert(Type::Never); return; @@ -1027,9 +1016,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { // ~S & ~T = ~T if S <: T - if existing_negative.is_subtype_of(db, new_negative) - || existing_negative.is_equivalent_to(db, new_negative) - { + if existing_negative.is_redundant_in_union_with(db, new_negative) { to_remove.push(index); } // same rule, reverse order @@ -1043,7 +1030,7 @@ impl<'db> InnerIntersectionBuilder<'db> { for existing_positive in &self.positive { // S & ~T = Never if S <: T - if existing_positive.is_subtype_of(db, new_negative) { + if existing_positive.is_redundant_in_union_with(db, new_negative) { *self = Self::default(); self.positive.insert(Type::Never); return; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 0afb14480a..9ccdd88a69 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -551,7 +551,9 @@ impl<'db> ClassType<'db> { self.iter_mro(db).when_any(db, |base| { match base { ClassBase::Dynamic(_) => match relation { - TypeRelation::Subtyping => ConstraintSet::from(other.is_object(db)), + TypeRelation::Subtyping | TypeRelation::UnionSimplification => { + ConstraintSet::from(other.is_object(db)) + } TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)), }, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index ed10c7b819..08dbb066b2 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -952,7 +952,9 @@ impl<'db> FunctionType<'db> { _visitor: &HasRelationToVisitor<'db>, ) -> ConstraintSet<'db> { match relation { - TypeRelation::Subtyping => ConstraintSet::from(self.is_subtype_of(db, other)), + TypeRelation::Subtyping | TypeRelation::UnionSimplification => { + ConstraintSet::from(self.is_subtype_of(db, other)) + } TypeRelation::Assignability => ConstraintSet::from(self.is_assignable_to(db, other)), } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 3eb227b647..de8746113f 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -567,7 +567,9 @@ fn has_relation_in_invariant_position<'db>( ), // Subtyping between invariant type parameters without a top/bottom materialization involved // is equivalence - (None, None, TypeRelation::Subtyping) => derived_type.when_equivalent_to(db, *base_type), + (None, None, TypeRelation::Subtyping | TypeRelation::UnionSimplification) => { + derived_type.when_equivalent_to(db, *base_type) + } (None, None, TypeRelation::Assignability) => derived_type .has_relation_to_impl(db, *base_type, TypeRelation::Assignability, visitor) .and(db, || { @@ -579,22 +581,26 @@ fn has_relation_in_invariant_position<'db>( ) }), // For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B] - (None, Some(base_mat), TypeRelation::Subtyping) => is_subtype_in_invariant_position( - db, - derived_type, - MaterializationKind::Top, - base_type, - base_mat, - visitor, - ), - (Some(derived_mat), None, TypeRelation::Subtyping) => is_subtype_in_invariant_position( - db, - derived_type, - derived_mat, - base_type, - MaterializationKind::Bottom, - visitor, - ), + (None, Some(base_mat), TypeRelation::Subtyping | TypeRelation::UnionSimplification) => { + is_subtype_in_invariant_position( + db, + derived_type, + MaterializationKind::Top, + base_type, + base_mat, + visitor, + ) + } + (Some(derived_mat), None, TypeRelation::Subtyping | TypeRelation::UnionSimplification) => { + is_subtype_in_invariant_position( + db, + derived_type, + derived_mat, + base_type, + MaterializationKind::Bottom, + visitor, + ) + } // And A <~ B (assignability) is Bottom[A] <: Top[B] (None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position( db, diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 1b0db0c808..d1bef69052 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -138,7 +138,7 @@ impl<'db> SubclassOfType<'db> { ) -> ConstraintSet<'db> { match (self.subclass_of, other.subclass_of) { (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from(relation.is_assignability()) + ConstraintSet::from(!relation.is_subtyping()) } (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => { ConstraintSet::from(other_class.is_object(db) || relation.is_assignability())