[ty] Less eager simplification of intersections that include constrained type variables

This commit is contained in:
Alex Waygood 2025-11-02 00:31:07 -04:00
parent bff32a41dc
commit 349e24db9a
3 changed files with 71 additions and 152 deletions

View File

@ -1008,10 +1008,10 @@ def constrained[T: (Base, Sub, Unrelated)](t: T) -> None:
reveal_type(x) # revealed: T@constrained & Base reveal_type(x) # revealed: T@constrained & Base
def _(x: Intersection[T, Unrelated]) -> None: def _(x: Intersection[T, Unrelated]) -> None:
reveal_type(x) # revealed: Unrelated reveal_type(x) # revealed: T@constrained & Unrelated
def _(x: Intersection[T, Sub]) -> None: def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: Sub reveal_type(x) # revealed: T@constrained & Sub
def _(x: Intersection[T, None]) -> None: def _(x: Intersection[T, None]) -> None:
reveal_type(x) # revealed: Never reveal_type(x) # revealed: Never
@ -1028,7 +1028,7 @@ from ty_extensions import Not
def remove_constraint[T: (int, str, bool)](t: T) -> None: def remove_constraint[T: (int, str, bool)](t: T) -> None:
def _(x: Intersection[T, Not[int]]) -> None: def _(x: Intersection[T, Not[int]]) -> None:
reveal_type(x) # revealed: str reveal_type(x) # revealed: T@remove_constraint & ~int
def _(x: Intersection[T, Not[str]]) -> None: def _(x: Intersection[T, Not[str]]) -> None:
# With OneOf this would be OneOf[int, bool] # With OneOf this would be OneOf[int, bool]
@ -1082,38 +1082,38 @@ class R: ...
def f[T: (P, Q)](t: T) -> None: def f[T: (P, Q)](t: T) -> None:
if isinstance(t, P): if isinstance(t, P):
reveal_type(t) # revealed: P reveal_type(t) # revealed: T@f & P
p: P = t p: P = t
else: else:
reveal_type(t) # revealed: Q & ~P reveal_type(t) # revealed: T@f & ~P
q: Q = t q: Q = t
if isinstance(t, Q): if isinstance(t, Q):
reveal_type(t) # revealed: Q reveal_type(t) # revealed: T@f & Q
q: Q = t q: Q = t
else: else:
reveal_type(t) # revealed: P & ~Q reveal_type(t) # revealed: T@f & ~Q
p: P = t p: P = t
def g[T: (P, Q, R)](t: T) -> None: def g[T: (P, Q, R)](t: T) -> None:
if isinstance(t, P): if isinstance(t, P):
reveal_type(t) # revealed: P reveal_type(t) # revealed: T@g & P
p: P = t p: P = t
elif isinstance(t, Q): elif isinstance(t, Q):
reveal_type(t) # revealed: Q & ~P reveal_type(t) # revealed: T@g & Q & ~P
q: Q = t q: Q = t
else: else:
reveal_type(t) # revealed: R & ~P & ~Q reveal_type(t) # revealed: T@g & ~P & ~Q
r: R = t r: R = t
if isinstance(t, P): if isinstance(t, P):
reveal_type(t) # revealed: P reveal_type(t) # revealed: T@g & P
p: P = t p: P = t
elif isinstance(t, Q): elif isinstance(t, Q):
reveal_type(t) # revealed: Q & ~P reveal_type(t) # revealed: T@g & Q & ~P
q: Q = t q: Q = t
elif isinstance(t, R): elif isinstance(t, R):
reveal_type(t) # revealed: R & ~P & ~Q reveal_type(t) # revealed: T@g & R & ~P & ~Q
r: R = t r: R = t
else: else:
reveal_type(t) # revealed: Never reveal_type(t) # revealed: Never
@ -1124,10 +1124,10 @@ If the constraints are disjoint, simplification does eliminate the redundant neg
```py ```py
def h[T: (P, None)](t: T) -> None: def h[T: (P, None)](t: T) -> None:
if t is None: if t is None:
reveal_type(t) # revealed: None reveal_type(t) # revealed: T@h & None
p: None = t p: None = t
else: else:
reveal_type(t) # revealed: P reveal_type(t) # revealed: T@h & ~None
p: P = t p: P = t
``` ```

View File

@ -1899,8 +1899,10 @@ impl<'db> Type<'db> {
}) })
}), }),
(Type::Intersection(intersection), _) => { (Type::Intersection(intersection), _) => intersection
intersection.positive(db).iter().when_any(db, |&elem_ty| { .positive(db)
.iter()
.when_any(db, |&elem_ty| {
elem_ty.has_relation_to_impl( elem_ty.has_relation_to_impl(
db, db,
target, target,
@ -1910,7 +1912,26 @@ impl<'db> Type<'db> {
disjointness_visitor, disjointness_visitor,
) )
}) })
.or(db, || {
if intersection
.positive(db)
.iter()
.any(|element| element.is_type_var())
{
intersection
.with_positive_typevars_solved_to_bounds_or_constraints(db)
.has_relation_to_impl(
db,
target,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
} else {
ConstraintSet::from(false)
} }
}),
// Other than the special cases checked above, no other types are a subtype of a // Other than the special cases checked above, no other types are a subtype of a
// typevar, since there's no guarantee what type the typevar will be specialized to. // typevar, since there's no guarantee what type the typevar will be specialized to.
@ -11804,6 +11825,22 @@ impl<'db> IntersectionType<'db> {
(self.positive(db).len() + self.negative(db).len()) == 1 (self.positive(db).len() + self.negative(db).len()) == 1
} }
pub(crate) fn with_positive_typevars_solved_to_bounds_or_constraints(
self,
db: &'db dyn Db,
) -> Type<'db> {
self.map_positive(db, |ty| match ty {
Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound,
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
Type::Union(constraints)
}
None => Type::object(),
},
_ => *ty,
})
}
fn heap_size((positive, negative): &(FxOrderSet<Type<'db>>, FxOrderSet<Type<'db>>)) -> usize { fn heap_size((positive, negative): &(FxOrderSet<Type<'db>>, FxOrderSet<Type<'db>>)) -> usize {
ruff_memory_usage::order_set_heap_size(positive) ruff_memory_usage::order_set_heap_size(positive)
+ ruff_memory_usage::order_set_heap_size(negative) + ruff_memory_usage::order_set_heap_size(negative)

View File

@ -40,8 +40,7 @@
use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::enums::{enum_member_literals, enum_metadata};
use crate::types::type_ordering::union_or_intersection_elements_ordering; use crate::types::type_ordering::union_or_intersection_elements_ordering;
use crate::types::{ use crate::types::{
BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type, BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type, UnionType,
TypeVarBoundOrConstraints, UnionType,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -1047,145 +1046,28 @@ impl<'db> InnerIntersectionBuilder<'db> {
} }
} }
/// Tries to simplify any constrained typevars in the intersection:
///
/// - If the intersection contains a positive entry for exactly one of the constraints, we can
/// remove the typevar (effectively replacing it with that one positive constraint).
///
/// - If the intersection contains negative entries for all but one of the constraints, we can
/// remove the negative constraints and replace the typevar with the remaining positive
/// constraint.
///
/// - If the intersection contains negative entries for all of the constraints, the overall
/// intersection is `Never`.
fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) {
let mut to_add = SmallVec::<[Type<'db>; 1]>::new();
let mut positive_to_remove = SmallVec::<[usize; 1]>::new();
for (typevar_index, ty) in self.positive.iter().enumerate() {
let Type::TypeVar(bound_typevar) = ty else {
continue;
};
let Some(TypeVarBoundOrConstraints::Constraints(constraints)) =
bound_typevar.typevar(db).bound_or_constraints(db)
else {
continue;
};
// Determine which constraints appear as positive entries in the intersection. Note
// that we shouldn't have duplicate entries in the positive or negative lists, so we
// don't need to worry about finding any particular constraint more than once.
let constraints = constraints.elements(db);
let mut positive_constraint_count = 0;
for (i, positive) in self.positive.iter().enumerate() {
if i == typevar_index {
continue;
}
// This linear search should be fine as long as we don't encounter typevars with
// thousands of constraints.
positive_constraint_count += constraints
.iter()
.filter(|c| c.is_subtype_of(db, *positive))
.count();
}
// If precisely one constraint appears as a positive element, we can replace the
// typevar with that positive constraint.
if positive_constraint_count == 1 {
positive_to_remove.push(typevar_index);
continue;
}
// Determine which constraints appear as negative entries in the intersection.
let mut to_remove = Vec::with_capacity(constraints.len());
let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect();
for (negative_index, negative) in self.negative.iter().enumerate() {
// This linear search should be fine as long as we don't encounter typevars with
// thousands of constraints.
let matching_constraints = constraints
.iter()
.enumerate()
.filter(|(_, c)| c.is_subtype_of(db, *negative));
for (constraint_index, _) in matching_constraints {
to_remove.push(negative_index);
remaining_constraints[constraint_index] = None;
}
}
let mut iter = remaining_constraints.into_iter().flatten();
let Some(remaining_constraint) = iter.next() else {
// All of the typevar constraints have been removed, so the entire intersection is
// `Never`.
*self = Self::default();
self.positive.insert(Type::Never);
return;
};
let more_than_one_remaining_constraint = iter.next().is_some();
if more_than_one_remaining_constraint {
// This typevar cannot be simplified.
continue;
}
// Only one typevar constraint remains. Remove all of the negative constraints, and
// replace the typevar itself with the remaining positive constraint.
to_add.push(remaining_constraint);
positive_to_remove.push(typevar_index);
}
// We don't need to sort the positive list, since we only append to it in increasing order.
for index in positive_to_remove.into_iter().rev() {
self.positive.swap_remove_index(index);
}
for remaining_constraint in to_add {
self.add_positive(db, remaining_constraint);
}
}
fn build(mut self, db: &'db dyn Db) -> Type<'db> { fn build(mut self, db: &'db dyn Db) -> Type<'db> {
self.simplify_constrained_typevars(db);
// If any typevars are in `self.positive`, speculatively solve all bounded type variables
// to their upper bound and all constrained type variables to the union of their constraints.
// If that speculative intersection simplifies to `Never`, this intersection must also simplify
// to `Never`.
if self.positive.iter().any(|ty| ty.is_type_var()) {
let mut speculative = IntersectionBuilder::new(db);
for pos in &self.positive {
match pos {
Type::TypeVar(type_var) => {
match type_var.typevar(db).bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
speculative = speculative.add_positive(bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
speculative = speculative.add_positive(Type::Union(constraints));
}
// TypeVars without a bound or constraint implicitly have `object` as their
// upper bound, and it is always a no-op to add `object` to an intersection.
None => {}
}
}
_ => speculative = speculative.add_positive(*pos),
}
}
for neg in &self.negative {
speculative = speculative.add_negative(*neg);
}
if speculative.build().is_never() {
return Type::Never;
}
}
match (self.positive.len(), self.negative.len()) { match (self.positive.len(), self.negative.len()) {
(0, 0) => Type::object(), (0, 0) => Type::object(),
(1, 0) => self.positive[0], (1, 0) => self.positive[0],
_ => { _ => {
self.positive.shrink_to_fit(); self.positive.shrink_to_fit();
self.negative.shrink_to_fit(); self.negative.shrink_to_fit();
Type::Intersection(IntersectionType::new(db, self.positive, self.negative))
let any_typevars_present =
self.positive.iter().any(|element| element.is_type_var());
let intersection = IntersectionType::new(db, self.positive, self.negative);
if any_typevars_present
&& intersection
.with_positive_typevars_solved_to_bounds_or_constraints(db)
.is_never()
{
Type::Never
} else {
Type::Intersection(intersection)
}
} }
} }
} }