Skip to content

Commit a8039f8

Browse files
dcreagerAlexWaygoodcarljm
authored
[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>
1 parent 5c2d4d8 commit a8039f8

File tree

7 files changed

+1098
-69
lines changed

7 files changed

+1098
-69
lines changed

crates/ty_python_semantic/src/types.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use crate::suppression::check_suppressions;
3939
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
4040
pub(crate) use crate::types::class_base::ClassBase;
4141
use crate::types::constraints::{
42-
Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
42+
ConstraintSet, Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
4343
};
4444
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
4545
use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
@@ -1360,7 +1360,8 @@ impl<'db> Type<'db> {
13601360
/// intersection simplification dependent on the order in which elements are added), so we do
13611361
/// not use this more general definition of subtyping.
13621362
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
1363-
self.when_subtype_of(db, target)
1363+
self.when_subtype_of::<ConstraintSet>(db, target)
1364+
.is_always_satisfied(db)
13641365
}
13651366

13661367
fn when_subtype_of<C: Constraints<'db>>(self, db: &'db dyn Db, target: Type<'db>) -> C {
@@ -1371,7 +1372,8 @@ impl<'db> Type<'db> {
13711372
///
13721373
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
13731374
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
1374-
self.when_assignable_to(db, target)
1375+
self.when_assignable_to::<ConstraintSet>(db, target)
1376+
.is_always_satisfied(db)
13751377
}
13761378

13771379
fn when_assignable_to<C: Constraints<'db>>(self, db: &'db dyn Db, target: Type<'db>) -> C {
@@ -1849,7 +1851,8 @@ impl<'db> Type<'db> {
18491851
///
18501852
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
18511853
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
1852-
self.when_equivalent_to(db, other)
1854+
self.when_equivalent_to::<ConstraintSet>(db, other)
1855+
.is_always_satisfied(db)
18531856
}
18541857

18551858
fn when_equivalent_to<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C {
@@ -1949,7 +1952,8 @@ impl<'db> Type<'db> {
19491952
/// Note: This function aims to have no false positives, but might return
19501953
/// wrong `false` answers in some cases.
19511954
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
1952-
self.when_disjoint_from(db, other)
1955+
self.when_disjoint_from::<ConstraintSet>(db, other)
1956+
.is_always_satisfied(db)
19531957
}
19541958

19551959
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>
97019705
}
97029706

97039707
impl<'db> IntersectionType<'db> {
9708+
pub(crate) fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
9709+
where
9710+
I: IntoIterator<Item = T>,
9711+
T: Into<Type<'db>>,
9712+
{
9713+
IntersectionBuilder::new(db)
9714+
.positive_elements(elements)
9715+
.build()
9716+
}
9717+
97049718
/// Return a new `IntersectionType` instance with the positive and negative types sorted
97059719
/// according to a canonical ordering, and other normalizations applied to each element as applicable.
97069720
///

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::db::Db;
1717
use crate::dunder_all::dunder_all_names;
1818
use crate::place::{Boundness, Place};
1919
use crate::types::call::arguments::{Expansion, is_expandable_type};
20+
use crate::types::constraints::{ConstraintSet, Constraints};
2021
use crate::types::diagnostic::{
2122
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
2223
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
@@ -2198,7 +2199,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
21982199
argument_type.apply_specialization(self.db, inherited_specialization);
21992200
expected_ty = expected_ty.apply_specialization(self.db, inherited_specialization);
22002201
}
2201-
if !argument_type.is_assignable_to(self.db, expected_ty) {
2202+
// This is one of the few places where we want to check if there's _any_ specialization
2203+
// where assignability holds; normally we want to check that assignability holds for
2204+
// _all_ specializations.
2205+
// TODO: Soon we will go further, and build the actual specializations from the
2206+
// constraint set that we get from this assignability check, instead of inferring and
2207+
// building them in an earlier separate step.
2208+
if argument_type
2209+
.when_assignable_to::<ConstraintSet>(self.db, expected_ty)
2210+
.is_never_satisfied(self.db)
2211+
{
22022212
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
22032213
&& !parameter.is_variadic();
22042214
self.errors.push(BindingError::InvalidArgumentType {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::semantic_index::{
1818
BindingWithConstraints, DeclarationWithConstraint, SemanticIndex, attribute_declarations,
1919
attribute_scopes,
2020
};
21-
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
21+
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
2222
use crate::types::context::InferContext;
2323
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
2424
use crate::types::enums::enum_metadata;
@@ -552,7 +552,8 @@ impl<'db> ClassType<'db> {
552552

553553
/// Return `true` if `other` is present in this class's MRO.
554554
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
555-
self.when_subclass_of(db, other)
555+
self.when_subclass_of::<ConstraintSet>(db, other)
556+
.is_always_satisfied(db)
556557
}
557558

558559
pub(super) fn when_subclass_of<C: Constraints<'db>>(

0 commit comments

Comments
 (0)