[ty] Introduce TypeRelation::UnionSimplification

This commit is contained in:
Alex Waygood 2025-09-27 16:15:53 +01:00
parent 4e33501115
commit 463b5dc3ee
6 changed files with 78 additions and 55 deletions

View file

@ -1050,13 +1050,6 @@ impl<'db> Type<'db> {
}
}
pub(crate) const fn into_intersection(self) -> Option<IntersectionType<'db>> {
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)]

View file

@ -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;

View file

@ -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)),
},

View file

@ -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)),
}
}

View file

@ -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(
(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) => is_subtype_in_invariant_position(
)
}
(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,

View file

@ -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())