[ty] Add constraint set implementation (#19997)

This PR adds an implementation of constraint sets.

An individual constraint restricts the specialization of a single
typevar to be within a particular lower and upper bound: the typevar can
only specialize to types that are a supertype of the lower bound, and a
subtype of the upper bound. (Note that lower and upper bounds are fully
static; we take the bottom and top materializations of the bounds to
remove any gradual forms if needed.) Either bound can be “closed” (where
the bound is a valid specialization), or “open” (where it is not).

You can then build up more complex constraint sets using union,
intersection, and negation operations. We use a disjunctive normal form
(DNF) representation, just like we do for types: a _constraint set_ is
the union of zero or more _clauses_, each of which is the intersection
of zero or more individual constraints. Note that the constraint set
that contains no clauses is never satisfiable (`⋃ {} = 0`); and the
constraint set that contains a single clause, which contains no
constraints, is always satisfiable (`⋃ {⋂ {}} = 1`).

One thing to note is that this PR does not change the logic of the
actual assignability checks, and in particular, we still aren't ever
trying to create an "individual constraint" that constrains a typevar.
Technically we're still operating only on `bool`s, since we only ever
instantiate `C::always_satisfiable` (i.e., `true`) and
`C::unsatisfiable` (i.e., `false`) in the `has_relation_to` methods. So
if you thought that #19838 introduced an unnecessarily complex stand-in
for `bool`, well here you go, this one is worse! (But still seemingly
not yielding a performance regression!) The next PR in this series,
#20093, is where we will actually create some non-trivial constraint
sets and use them in anger.

That said, the PR does go ahead and update the assignability checks to
use the new `ConstraintSet` type instead of `bool`. That part is fairly
straightforward since we had already updated the assignability checks to
use the `Constraints` trait; we just have to actively choose a different
impl type. (For the `is_whatever` variants, which still return a `bool`,
we have to convert the constraint set, but the explicit
`is_always_satisfiable` calls serve as nice documentation of our
intent.)

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-08-28 20:04:29 -04:00 committed by GitHub
parent 5c2d4d8d8f
commit a8039f80f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1098 additions and 69 deletions

View file

@ -39,7 +39,7 @@ use crate::suppression::check_suppressions;
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::constraints::{
Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
ConstraintSet, Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
};
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
@ -1360,7 +1360,8 @@ impl<'db> Type<'db> {
/// intersection simplification dependent on the order in which elements are added), so we do
/// not use this more general definition of subtyping.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_subtype_of(db, target)
self.when_subtype_of::<ConstraintSet>(db, target)
.is_always_satisfied(db)
}
fn when_subtype_of<C: Constraints<'db>>(self, db: &'db dyn Db, target: Type<'db>) -> C {
@ -1371,7 +1372,8 @@ impl<'db> Type<'db> {
///
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_assignable_to(db, target)
self.when_assignable_to::<ConstraintSet>(db, target)
.is_always_satisfied(db)
}
fn when_assignable_to<C: Constraints<'db>>(self, db: &'db dyn Db, target: Type<'db>) -> C {
@ -1849,7 +1851,8 @@ impl<'db> Type<'db> {
///
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.when_equivalent_to(db, other)
self.when_equivalent_to::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
fn when_equivalent_to<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C {
@ -1949,7 +1952,8 @@ impl<'db> Type<'db> {
/// Note: This function aims to have no false positives, but might return
/// wrong `false` answers in some cases.
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.when_disjoint_from(db, other)
self.when_disjoint_from::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
fn when_disjoint_from<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C {
@ -9701,6 +9705,16 @@ pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>
}
impl<'db> IntersectionType<'db> {
pub(crate) fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
where
I: IntoIterator<Item = T>,
T: Into<Type<'db>>,
{
IntersectionBuilder::new(db)
.positive_elements(elements)
.build()
}
/// Return a new `IntersectionType` instance with the positive and negative types sorted
/// according to a canonical ordering, and other normalizations applied to each element as applicable.
///

View file

@ -17,6 +17,7 @@ use crate::db::Db;
use crate::dunder_all::dunder_all_names;
use crate::place::{Boundness, Place};
use crate::types::call::arguments::{Expansion, is_expandable_type};
use crate::types::constraints::{ConstraintSet, Constraints};
use crate::types::diagnostic::{
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
@ -2198,7 +2199,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
argument_type.apply_specialization(self.db, inherited_specialization);
expected_ty = expected_ty.apply_specialization(self.db, inherited_specialization);
}
if !argument_type.is_assignable_to(self.db, expected_ty) {
// This is one of the few places where we want to check if there's _any_ specialization
// where assignability holds; normally we want to check that assignability holds for
// _all_ specializations.
// TODO: Soon we will go further, and build the actual specializations from the
// constraint set that we get from this assignability check, instead of inferring and
// building them in an earlier separate step.
if argument_type
.when_assignable_to::<ConstraintSet>(self.db, expected_ty)
.is_never_satisfied(self.db)
{
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
&& !parameter.is_variadic();
self.errors.push(BindingError::InvalidArgumentType {

View file

@ -18,7 +18,7 @@ use crate::semantic_index::{
BindingWithConstraints, DeclarationWithConstraint, SemanticIndex, attribute_declarations,
attribute_scopes,
};
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::enums::enum_metadata;
@ -552,7 +552,8 @@ impl<'db> ClassType<'db> {
/// Return `true` if `other` is present in this class's MRO.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.when_subclass_of(db, other)
self.when_subclass_of::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
pub(super) fn when_subclass_of<C: Constraints<'db>>(

File diff suppressed because it is too large Load diff

View file

@ -16,8 +16,9 @@ use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec;
use crate::types::{
CallableType, IntersectionType, KnownClass, MaterializationKind, MethodWrapperKind, Protocol,
StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind,
BoundTypeVarInstance, CallableType, IntersectionType, KnownClass, MaterializationKind,
MethodWrapperKind, Protocol, StringLiteralType, SubclassOfInner, Type, UnionType,
WrapperDescriptorKind,
};
use ruff_db::parsed::parsed_module;
@ -360,12 +361,7 @@ impl Display for DisplayRepresentation<'_> {
f.write_str(enum_literal.name(self.db))
}
Type::NonInferableTypeVar(bound_typevar) | Type::TypeVar(bound_typevar) => {
f.write_str(bound_typevar.typevar(self.db).name(self.db))?;
if let Some(binding_context) = bound_typevar.binding_context(self.db).name(self.db)
{
write!(f, "@{binding_context}")?;
}
Ok(())
bound_typevar.display(self.db).fmt(f)
}
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
@ -402,6 +398,30 @@ impl Display for DisplayRepresentation<'_> {
}
}
impl<'db> BoundTypeVarInstance<'db> {
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
DisplayBoundTypeVarInstance {
bound_typevar: self,
db,
}
}
}
struct DisplayBoundTypeVarInstance<'db> {
bound_typevar: BoundTypeVarInstance<'db>,
db: &'db dyn Db,
}
impl Display for DisplayBoundTypeVarInstance<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.bound_typevar.typevar(self.db).name(self.db))?;
if let Some(binding_context) = self.bound_typevar.binding_context(self.db).name(self.db) {
write!(f, "@{binding_context}")?;
}
Ok(())
}
}
impl<'db> TupleSpec<'db> {
pub(crate) fn display_with(
&'db self,

View file

@ -7,7 +7,7 @@ use super::protocol_class::ProtocolInterface;
use super::{BoundTypeVarInstance, ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::PlaceAndQualifiers;
use crate::semantic_index::definition::Definition;
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::enums::is_single_member_enum;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::{TupleSpec, TupleType};
@ -513,12 +513,15 @@ impl<'db> ProtocolInstanceType<'db> {
visitor: &NormalizedVisitor<'db>,
) -> Type<'db> {
let object = Type::object(db);
if object.satisfies_protocol(
if object
.satisfies_protocol(
db,
self,
TypeRelation::Subtyping,
&HasRelationToVisitor::new(true),
) {
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
)
.is_always_satisfied(db)
{
return object;
}
match self.inner {

View file

@ -17,7 +17,7 @@ use smallvec::{SmallVec, smallvec_inline};
use super::{DynamicType, Type, TypeVarVariance, definition_expression_type};
use crate::semantic_index::definition::Definition;
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::generics::{GenericContext, walk_generic_context};
use crate::types::{
BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
@ -123,7 +123,8 @@ impl<'db> CallableSignature<'db> {
///
/// See [`Type::is_subtype_of`] for more details.
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
self.is_subtype_of_impl(db, other)
self.is_subtype_of_impl::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
fn is_subtype_of_impl<C: Constraints<'db>>(&self, db: &'db dyn Db, other: &Self) -> C {
@ -143,8 +144,9 @@ impl<'db> CallableSignature<'db> {
db,
other,
TypeRelation::Assignability,
&HasRelationToVisitor::new(true),
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
)
.is_always_satisfied(db)
}
pub(crate) fn has_relation_to_impl<C: Constraints<'db>>(