[ty] Add and test when constraint sets are satisfied by their typevars (#21129)

This PR adds a new `satisfied_by_all_typevar` method, which implements
one of the final steps of actually using these dang constraint sets.
Constraint sets exist to help us check assignability and subtyping of
types in the presence of typevars. We construct a constraint set
describing the conditions under which assignability holds between the
two types. Then we check whether that constraint set is satisfied for
the valid specializations of the relevant typevars (which is this new
method).

We also add a new `ty_extensions.ConstraintSet` method so that we can
test this method's behavior in mdtests, before hooking it up to the rest
of the specialization inference machinery.
This commit is contained in:
Douglas Creager
2025-10-31 10:53:37 -04:00
committed by GitHub
parent 1baf98aab3
commit cf4e82d4b0
6 changed files with 425 additions and 16 deletions

View File

@@ -4161,6 +4161,14 @@ impl<'db> Type<'db> {
))
.into()
}
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
if name == "satisfied_by_all_typevars" =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked),
))
.into()
}
Type::ClassLiteral(class)
if name == "__get__" && class.is_known(db, KnownClass::FunctionType) =>
@@ -6923,6 +6931,7 @@ impl<'db> Type<'db> {
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@@ -7074,7 +7083,8 @@ impl<'db> Type<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@@ -10339,6 +10349,7 @@ pub enum KnownBoundMethodType<'db> {
ConstraintSetAlways,
ConstraintSetNever,
ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
ConstraintSetSatisfiedByAllTypeVars(TrackedConstraintSet<'db>),
}
pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -10366,7 +10377,8 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {}
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {}
}
}
@@ -10434,6 +10446,10 @@ impl<'db> KnownBoundMethodType<'db> {
| (
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
)
| (
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(true),
(
@@ -10446,7 +10462,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
@@ -10456,7 +10473,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(false),
}
}
@@ -10509,6 +10527,10 @@ impl<'db> KnownBoundMethodType<'db> {
(
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints),
)
| (
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(left_constraints),
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(right_constraints),
) => left_constraints
.constraints(db)
.iff(db, right_constraints.constraints(db)),
@@ -10523,7 +10545,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
@@ -10533,7 +10556,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(false),
}
}
@@ -10557,7 +10581,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => self,
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => self,
}
}
@@ -10573,7 +10598,10 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => KnownClass::ConstraintSet,
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
KnownClass::ConstraintSet
}
}
}
@@ -10712,6 +10740,19 @@ impl<'db> KnownBoundMethodType<'db> {
Some(KnownClass::ConstraintSet.to_instance(db)),
)))
}
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
Either::Right(std::iter::once(Signature::new(
Parameters::new([Parameter::keyword_only(Name::new_static("inferable"))
.type_form()
.with_annotated_type(UnionType::from_elements(
db,
[Type::homogeneous_tuple(db, Type::any()), Type::none(db)],
))
.with_default_type(Type::none(db))]),
Some(KnownClass::Bool.to_instance(db)),
)))
}
}
}
}

View File

@@ -9,6 +9,7 @@ use std::fmt;
use itertools::{Either, Itertools};
use ruff_db::parsed::parsed_module;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;
use smallvec::{SmallVec, smallvec, smallvec_inline};
use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type};
@@ -35,9 +36,10 @@ use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Paramete
use crate::types::tuple::{TupleLength, TupleType};
use crate::types::{
BoundMethodType, ClassLiteral, DataclassFlags, DataclassParams, FieldInstance,
KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, PropertyInstanceType,
SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, UnionBuilder, UnionType,
WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type,
KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, NominalInstanceType,
PropertyInstanceType, SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext,
UnionBuilder, UnionType, WrapperDescriptorKind, enums, ide_support, infer_isolated_expression,
todo_type,
};
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
@@ -1174,6 +1176,42 @@ impl<'db> Bindings<'db> {
));
}
Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked),
) => {
let extract_inferable = |instance: &NominalInstanceType<'db>| {
if instance.has_known_class(db, KnownClass::NoneType) {
// Caller explicitly passed None, so no typevars are inferable.
return Some(FxHashSet::default());
}
instance
.tuple_spec(db)?
.fixed_elements()
.map(|ty| {
ty.as_typevar()
.map(|bound_typevar| bound_typevar.identity(db))
})
.collect()
};
let inferable = match overload.parameter_types() {
// Caller did not provide argument, so no typevars are inferable.
[None] => FxHashSet::default(),
[Some(Type::NominalInstance(instance))] => {
match extract_inferable(instance) {
Some(inferable) => inferable,
None => continue,
}
}
_ => continue,
};
let result = tracked
.constraints(db)
.satisfied_by_all_typevars(db, InferableTypeVars::One(&inferable));
overload.set_return_type(Type::BooleanLiteral(result));
}
Type::ClassLiteral(class) => match class.known(db) {
Some(KnownClass::Bool) => match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),

View File

@@ -65,7 +65,10 @@ use salsa::plumbing::AsId;
use crate::Db;
use crate::types::generics::InferableTypeVars;
use crate::types::{BoundTypeVarInstance, IntersectionType, Type, TypeRelation, UnionType};
use crate::types::{
BoundTypeVarInstance, IntersectionType, Type, TypeRelation, TypeVarBoundOrConstraints,
UnionType,
};
/// An extension trait for building constraint sets from [`Option`] values.
pub(crate) trait OptionConstraintsExtension<T> {
@@ -256,6 +259,28 @@ impl<'db> ConstraintSet<'db> {
}
}
/// Returns whether this constraint set is satisfied by all of the typevars that it mentions.
///
/// Each typevar has a set of _valid specializations_, which is defined by any upper bound or
/// constraints that the typevar has.
///
/// Each typevar is also either _inferable_ or _non-inferable_. (You provide a list of the
/// `inferable` typevars; all others are considered non-inferable.) For an inferable typevar,
/// then there must be _some_ valid specialization that satisfies the constraint set. For a
/// non-inferable typevar, then _all_ valid specializations must satisfy it.
///
/// Note that we don't have to consider typevars that aren't mentioned in the constraint set,
/// since the constraint set cannot be affected by any typevars that it does not mention. That
/// means that those additional typevars trivially satisfy the constraint set, regardless of
/// whether they are inferable or not.
pub(crate) fn satisfied_by_all_typevars(
self,
db: &'db dyn Db,
inferable: InferableTypeVars<'_, 'db>,
) -> bool {
self.node.satisfied_by_all_typevars(db, inferable)
}
/// Updates this constraint set to hold the union of itself and another constraint set.
pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self {
self.node = self.node.or(db, other.node);
@@ -746,6 +771,13 @@ impl<'db> Node<'db> {
.or(db, self.negate(db).and(db, else_node))
}
fn satisfies(self, db: &'db dyn Db, other: Self) -> Self {
let simplified_self = self.simplify(db);
let implication = simplified_self.implies(db, other);
let (simplified, domain) = implication.simplify_and_domain(db);
simplified.and(db, domain)
}
fn when_subtype_of_given(
self,
db: &'db dyn Db,
@@ -767,10 +799,48 @@ impl<'db> Node<'db> {
_ => return lhs.when_subtype_of(db, rhs, inferable).node,
};
let simplified_self = self.simplify(db);
let implication = simplified_self.implies(db, constraint);
let (simplified, domain) = implication.simplify_and_domain(db);
simplified.and(db, domain)
self.satisfies(db, constraint)
}
fn satisfied_by_all_typevars(
self,
db: &'db dyn Db,
inferable: InferableTypeVars<'_, 'db>,
) -> bool {
match self {
Node::AlwaysTrue => return true,
Node::AlwaysFalse => return false,
Node::Interior(_) => {}
}
let mut typevars = FxHashSet::default();
self.for_each_constraint(db, &mut |constraint| {
typevars.insert(constraint.typevar(db));
});
for typevar in typevars {
// Determine which valid specializations of this typevar satisfy the constraint set.
let valid_specializations = typevar.valid_specializations(db).node;
let when_satisfied = valid_specializations
.satisfies(db, self)
.and(db, valid_specializations);
let satisfied = if typevar.is_inferable(db, inferable) {
// If the typevar is inferable, then we only need one valid specialization to
// satisfy the constraint set.
!when_satisfied.is_never_satisfied()
} else {
// If the typevar is non-inferable, then we need _all_ valid specializations to
// satisfy the constraint set.
when_satisfied
.iff(db, valid_specializations)
.is_always_satisfied(db)
};
if !satisfied {
return false;
}
}
true
}
/// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to
@@ -1861,6 +1931,33 @@ impl<'db> SatisfiedClauses<'db> {
}
}
/// Returns a constraint set describing the valid specializations of a typevar.
impl<'db> BoundTypeVarInstance<'db> {
pub(crate) fn valid_specializations(self, db: &'db dyn Db) -> ConstraintSet<'db> {
match self.typevar(db).bound_or_constraints(db) {
None => ConstraintSet::from(true),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => ConstraintSet::constrain_typevar(
db,
self,
Type::Never,
bound,
TypeRelation::Assignability,
),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.elements(db).iter().when_any(db, |constraint| {
ConstraintSet::constrain_typevar(
db,
self,
*constraint,
*constraint,
TypeRelation::Assignability,
)
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -535,6 +535,9 @@ impl Display for DisplayRepresentation<'_> {
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => {
f.write_str("bound method `ConstraintSet.implies_subtype_of`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(
_,
)) => f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"),
Type::WrapperDescriptor(kind) => {
let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),