From b9691b8a074dd360b24e6bf2f6f73c7dbaacc5ea Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 26 Nov 2025 14:52:41 -0500 Subject: [PATCH] add ConstraintSetAssignability relation --- crates/ty_python_semantic/src/types.rs | 49 +++++++++++++++++-- crates/ty_python_semantic/src/types/class.rs | 4 +- .../src/types/constraints.rs | 2 +- .../ty_python_semantic/src/types/generics.rs | 12 ++++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d0efb0db19..e3813d4768 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1955,6 +1955,33 @@ impl<'db> Type<'db> { return constraints.implies_subtype_of(db, self, target); } + // Handle the new constraint-set-based assignability relation next. Comparisons with a + // typevar are translated directly into a constraint set. + if relation.is_constraint_set_assignability() { + // A typevar satisfies a relation when...it satisfies the relation. Yes that's a + // tautology! We're moving the caller's subtyping/assignability requirement into a + // constraint set. If the typevar has an upper bound or constraints, then the relation + // only has to hold when the typevar has a valid specialization (i.e., one that + // satisfies the upper bound/constraints). + if let Type::TypeVar(bound_typevar) = self { + return ConstraintSet::constrain_typevar( + db, + bound_typevar, + Type::Never, + target, + relation, + ); + } else if let Type::TypeVar(bound_typevar) = target { + return ConstraintSet::constrain_typevar( + db, + bound_typevar, + self, + Type::object(), + relation, + ); + } + } + match (self, target) { // Everything is a subtype of `object`. (_, Type::NominalInstance(instance)) if instance.is_object() => { @@ -2035,7 +2062,7 @@ impl<'db> Type<'db> { ); ConstraintSet::from(match relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false, - TypeRelation::Assignability => true, + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, TypeRelation::Redundancy => match target { Type::Dynamic(_) => true, Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), @@ -2045,7 +2072,7 @@ impl<'db> Type<'db> { } (_, Type::Dynamic(_)) => ConstraintSet::from(match relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false, - TypeRelation::Assignability => true, + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, TypeRelation::Redundancy => match self { Type::Dynamic(_) => true, Type::Intersection(intersection) => { @@ -2309,14 +2336,19 @@ impl<'db> Type<'db> { TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_) => self, - TypeRelation::Assignability => self.bottom_materialization(db), + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { + self.bottom_materialization(db) + } }; intersection.negative(db).iter().when_all(db, |&neg_ty| { let neg_ty = match relation { TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_) => neg_ty, - TypeRelation::Assignability => neg_ty.bottom_materialization(db), + TypeRelation::Assignability + | TypeRelation::ConstraintSetAssignability => { + neg_ty.bottom_materialization(db) + } }; self_ty.is_disjoint_from_impl( db, @@ -11860,6 +11892,11 @@ pub(crate) enum TypeRelation<'db> { /// are not actually subtypes of each other. (That is, `implies_subtype_of(false, int, str)` /// will return true!) SubtypingAssuming(ConstraintSet<'db>), + + /// A placeholder for the new assignability relation that uses constraint sets to encode + /// relationships with a typevar. This will eventually replace `Assignability`, but allows us + /// to start using the new relation in a controlled manner in some places. + ConstraintSetAssignability, } impl TypeRelation<'_> { @@ -11867,6 +11904,10 @@ impl TypeRelation<'_> { matches!(self, TypeRelation::Assignability) } + pub(crate) const fn is_constraint_set_assignability(self) -> bool { + matches!(self, TypeRelation::ConstraintSetAssignability) + } + pub(crate) const fn is_subtyping(self) -> bool { matches!(self, TypeRelation::Subtyping) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a5122431dd..e2a262d37d 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -637,7 +637,9 @@ impl<'db> ClassType<'db> { | TypeRelation::SubtypingAssuming(_) => { ConstraintSet::from(other.is_object(db)) } - TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)), + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { + ConstraintSet::from(!other.is_final(db)) + } }, // Protocol, Generic, and TypedDict are not represented by a ClassType. diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 7a727f3285..14b3c32218 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -207,7 +207,7 @@ impl<'db> ConstraintSet<'db> { lower.top_materialization(db), upper.bottom_materialization(db), ), - TypeRelation::Assignability => ( + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => ( lower.bottom_materialization(db), upper.top_materialization(db), ), diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 9d22d7fbfa..a0545abddc 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -903,7 +903,11 @@ fn has_relation_in_invariant_position<'db>( disjointness_visitor, ), // And A <~ B (assignability) is Bottom[A] <: Top[B] - (None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position( + ( + None, + Some(base_mat), + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability, + ) => is_subtype_in_invariant_position( db, derived_type, MaterializationKind::Bottom, @@ -913,7 +917,11 @@ fn has_relation_in_invariant_position<'db>( relation_visitor, disjointness_visitor, ), - (Some(derived_mat), None, TypeRelation::Assignability) => is_subtype_in_invariant_position( + ( + Some(derived_mat), + None, + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability, + ) => is_subtype_in_invariant_position( db, derived_type, derived_mat,