mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[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:
parent
5c2d4d8d8f
commit
a8039f80f0
7 changed files with 1098 additions and 69 deletions
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>>(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue