Skip to content

Commit eda85f3

Browse files
authored
[ty] Constraining a typevar with itself (possibly via union or intersection) (#21273)
This PR carries over some of the `has_relation_to` logic for comparing a typevar with itself. A typevar will specialize to the same type if it's mentioned multiple times, so it is always assignable to and a subtype of itself. (Note that typevars can only specialize to fully static types.) This is also true when the typevar appears in a union on the right-hand side, or in an intersection on the left-hand side. Similarly, a typevar is always disjoint from its negation, so when a negated typevar appears on the left-hand side, the constraint set is never satisfiable. (Eventually this will allow us to remove the corresponding clauses from `has_relation_to`, but that can't happen until more of #20093 lands.)
1 parent cef6600 commit eda85f3

File tree

6 files changed

+278
-10
lines changed

6 files changed

+278
-10
lines changed

crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md

Lines changed: 161 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,50 @@ def _[T]() -> None:
258258
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
259259
```
260260

261+
Expanding on this, when intersecting two upper bounds constraints (`(T ≤ Base) ∧ (T ≤ Other)`), we
262+
intersect the upper bounds. Any type that satisfies both `T ≤ Base` and `T ≤ Other` must necessarily
263+
satisfy their intersection `T ≤ Base & Other`, and vice versa.
264+
265+
```py
266+
from typing import Never
267+
from ty_extensions import Intersection, static_assert
268+
269+
# This is not final, so it's possible for a subclass to inherit from both Base and Other.
270+
class Other: ...
271+
272+
def upper_bounds[T]():
273+
intersection_type = ConstraintSet.range(Never, T, Intersection[Base, Other])
274+
# revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
275+
reveal_type(intersection_type)
276+
277+
intersection_constraint = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, T, Other)
278+
# revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
279+
reveal_type(intersection_constraint)
280+
281+
# The two constraint sets are equivalent; each satisfies the other.
282+
static_assert(intersection_type.satisfies(intersection_constraint))
283+
static_assert(intersection_constraint.satisfies(intersection_type))
284+
```
285+
286+
For an intersection of two lower bounds constraints (`(Base ≤ T) ∧ (Other ≤ T)`), we union the lower
287+
bounds. Any type that satisfies both `Base ≤ T` and `Other ≤ T` must necessarily satisfy their union
288+
`Base | Other ≤ T`, and vice versa.
289+
290+
```py
291+
def lower_bounds[T]():
292+
union_type = ConstraintSet.range(Base | Other, T, object)
293+
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
294+
reveal_type(union_type)
295+
296+
intersection_constraint = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Other, T, object)
297+
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
298+
reveal_type(intersection_constraint)
299+
300+
# The two constraint sets are equivalent; each satisfies the other.
301+
static_assert(union_type.satisfies(intersection_constraint))
302+
static_assert(intersection_constraint.satisfies(union_type))
303+
```
304+
261305
### Intersection of a range and a negated range
262306

263307
The bounds of the range constraint provide a range of types that should be included; the bounds of
@@ -335,7 +379,7 @@ def _[T]() -> None:
335379
reveal_type(~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super))
336380
```
337381

338-
Otherwise, the union cannot be simplified.
382+
Otherwise, the intersection cannot be simplified.
339383

340384
```py
341385
def _[T]() -> None:
@@ -350,13 +394,14 @@ def _[T]() -> None:
350394
In particular, the following does not simplify, even though it seems like it could simplify to
351395
`¬(SubSub ≤ T@_ ≤ Super)`. The issue is that there are types that are within the bounds of
352396
`SubSub ≤ T@_ ≤ Super`, but which are not comparable to `Base` or `Sub`, and which therefore should
353-
be included in the union. An example would be the type that contains all instances of `Super`,
354-
`Base`, and `SubSub` (but _not_ including instances of `Sub`). (We don't have a way to spell that
355-
type at the moment, but it is a valid type.) That type is not in `SubSub ≤ T ≤ Base`, since it
356-
includes `Super`, which is outside the range. It's also not in `Sub ≤ T ≤ Super`, because it does
357-
not include `Sub`. That means it should be in the union. (Remember that for negated range
358-
constraints, the lower and upper bounds define the "hole" of types that are _not_ allowed.) Since
359-
that type _is_ in `SubSub ≤ T ≤ Super`, it is not correct to simplify the union in this way.
397+
be included in the intersection. An example would be the type that contains all instances of
398+
`Super`, `Base`, and `SubSub` (but _not_ including instances of `Sub`). (We don't have a way to
399+
spell that type at the moment, but it is a valid type.) That type is not in `SubSub ≤ T ≤ Base`,
400+
since it includes `Super`, which is outside the range. It's also not in `Sub ≤ T ≤ Super`, because
401+
it does not include `Sub`. That means it should be in the intersection. (Remember that for negated
402+
range constraints, the lower and upper bounds define the "hole" of types that are _not_ allowed.)
403+
Since that type _is_ in `SubSub ≤ T ≤ Super`, it is not correct to simplify the intersection in this
404+
way.
360405

361406
```py
362407
def _[T]() -> None:
@@ -441,6 +486,65 @@ def _[T]() -> None:
441486
reveal_type(ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
442487
```
443488

489+
The union of two upper bound constraints (`(T ≤ Base) ∨ (T ≤ Other)`) is different than the single
490+
range constraint involving the corresponding union type (`T ≤ Base | Other`). There are types (such
491+
as `T = Base | Other`) that satisfy the union type, but not the union constraint. But every type
492+
that satisfies the union constraint satisfies the union type.
493+
494+
```py
495+
from typing import Never
496+
from ty_extensions import static_assert
497+
498+
# This is not final, so it's possible for a subclass to inherit from both Base and Other.
499+
class Other: ...
500+
501+
def union[T]():
502+
union_type = ConstraintSet.range(Never, T, Base | Other)
503+
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base | Other)]
504+
reveal_type(union_type)
505+
506+
union_constraint = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Other)
507+
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base) ∨ (T@union ≤ Other)]
508+
reveal_type(union_constraint)
509+
510+
# (T = Base | Other) satisfies (T ≤ Base | Other) but not (T ≤ Base ∨ T ≤ Other)
511+
specialization = ConstraintSet.range(Base | Other, T, Base | Other)
512+
# revealed: ty_extensions.ConstraintSet[(T@union = Base | Other)]
513+
reveal_type(specialization)
514+
static_assert(specialization.satisfies(union_type))
515+
static_assert(not specialization.satisfies(union_constraint))
516+
517+
# Every specialization that satisfies (T ≤ Base ∨ T ≤ Other) also satisfies
518+
# (T ≤ Base | Other)
519+
static_assert(union_constraint.satisfies(union_type))
520+
```
521+
522+
These relationships are reversed for unions involving lower bounds. `T = Base` is an example that
523+
satisfies the union constraint (`(Base ≤ T) ∨ (Other ≤ T)`) but not the union type
524+
(`Base | Other ≤ T`). And every type that satisfies the union type satisfies the union constraint.
525+
526+
```py
527+
def union[T]():
528+
union_type = ConstraintSet.range(Base | Other, T, object)
529+
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@union)]
530+
reveal_type(union_type)
531+
532+
union_constraint = ConstraintSet.range(Base, T, object) | ConstraintSet.range(Other, T, object)
533+
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@union) ∨ (Other ≤ T@union)]
534+
reveal_type(union_constraint)
535+
536+
# (T = Base) satisfies (Base ≤ T ∨ Other ≤ T) but not (Base | Other ≤ T)
537+
specialization = ConstraintSet.range(Base, T, Base)
538+
# revealed: ty_extensions.ConstraintSet[(T@union = Base)]
539+
reveal_type(specialization)
540+
static_assert(not specialization.satisfies(union_type))
541+
static_assert(specialization.satisfies(union_constraint))
542+
543+
# Every specialization that satisfies (Base | Other ≤ T) also satisfies
544+
# (Base ≤ T ∨ Other ≤ T)
545+
static_assert(union_type.satisfies(union_constraint))
546+
```
547+
444548
### Union of a range and a negated range
445549

446550
The bounds of the range constraint provide a range of types that should be included; the bounds of
@@ -729,3 +833,52 @@ def f[T]():
729833
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
730834
reveal_type(ConstraintSet.range(Never, T, int | str))
731835
```
836+
837+
### Constraints on the same typevar
838+
839+
Any particular specialization maps each typevar to one type. That means it's not useful to constrain
840+
a typevar with itself as an upper or lower bound. No matter what type the typevar is specialized to,
841+
that type is always a subtype of itself. (Remember that typevars are only specialized to fully
842+
static types.)
843+
844+
```py
845+
from typing import Never
846+
from ty_extensions import ConstraintSet
847+
848+
def same_typevar[T]():
849+
# revealed: ty_extensions.ConstraintSet[always]
850+
reveal_type(ConstraintSet.range(Never, T, T))
851+
# revealed: ty_extensions.ConstraintSet[always]
852+
reveal_type(ConstraintSet.range(T, T, object))
853+
# revealed: ty_extensions.ConstraintSet[always]
854+
reveal_type(ConstraintSet.range(T, T, T))
855+
```
856+
857+
This is also true when the typevar appears in a union in the upper bound, or in an intersection in
858+
the lower bound. (Note that this lines up with how we simplify the intersection of two constraints,
859+
as shown above.)
860+
861+
```py
862+
from ty_extensions import Intersection
863+
864+
def same_typevar[T]():
865+
# revealed: ty_extensions.ConstraintSet[always]
866+
reveal_type(ConstraintSet.range(Never, T, T | None))
867+
# revealed: ty_extensions.ConstraintSet[always]
868+
reveal_type(ConstraintSet.range(Intersection[T, None], T, object))
869+
# revealed: ty_extensions.ConstraintSet[always]
870+
reveal_type(ConstraintSet.range(Intersection[T, None], T, T | None))
871+
```
872+
873+
Similarly, if the lower bound is an intersection containing the _negation_ of the typevar, then the
874+
constraint set can never be satisfied, since every type is disjoint with its negation.
875+
876+
```py
877+
from ty_extensions import Not
878+
879+
def same_typevar[T]():
880+
# revealed: ty_extensions.ConstraintSet[never]
881+
reveal_type(ConstraintSet.range(Intersection[Not[T], None], T, object))
882+
# revealed: ty_extensions.ConstraintSet[never]
883+
reveal_type(ConstraintSet.range(Not[T], T, object))
884+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4197,6 +4197,14 @@ impl<'db> Type<'db> {
41974197
))
41984198
.into()
41994199
}
4200+
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
4201+
if name == "satisfies" =>
4202+
{
4203+
Place::bound(Type::KnownBoundMethod(
4204+
KnownBoundMethodType::ConstraintSetSatisfies(tracked),
4205+
))
4206+
.into()
4207+
}
42004208
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
42014209
if name == "satisfied_by_all_typevars" =>
42024210
{
@@ -6973,6 +6981,7 @@ impl<'db> Type<'db> {
69736981
| KnownBoundMethodType::ConstraintSetAlways
69746982
| KnownBoundMethodType::ConstraintSetNever
69756983
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
6984+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
69766985
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
69776986
)
69786987
| Type::DataclassDecorator(_)
@@ -7126,6 +7135,7 @@ impl<'db> Type<'db> {
71267135
| KnownBoundMethodType::ConstraintSetAlways
71277136
| KnownBoundMethodType::ConstraintSetNever
71287137
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
7138+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
71297139
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
71307140
)
71317141
| Type::DataclassDecorator(_)
@@ -10470,6 +10480,7 @@ pub enum KnownBoundMethodType<'db> {
1047010480
ConstraintSetAlways,
1047110481
ConstraintSetNever,
1047210482
ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
10483+
ConstraintSetSatisfies(TrackedConstraintSet<'db>),
1047310484
ConstraintSetSatisfiedByAllTypeVars(TrackedConstraintSet<'db>),
1047410485
}
1047510486

@@ -10499,6 +10510,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
1049910510
| KnownBoundMethodType::ConstraintSetAlways
1050010511
| KnownBoundMethodType::ConstraintSetNever
1050110512
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10513+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1050210514
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {}
1050310515
}
1050410516
}
@@ -10568,6 +10580,10 @@ impl<'db> KnownBoundMethodType<'db> {
1056810580
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
1056910581
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
1057010582
)
10583+
| (
10584+
KnownBoundMethodType::ConstraintSetSatisfies(_),
10585+
KnownBoundMethodType::ConstraintSetSatisfies(_),
10586+
)
1057110587
| (
1057210588
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
1057310589
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
@@ -10584,6 +10600,7 @@ impl<'db> KnownBoundMethodType<'db> {
1058410600
| KnownBoundMethodType::ConstraintSetAlways
1058510601
| KnownBoundMethodType::ConstraintSetNever
1058610602
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10603+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1058710604
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
1058810605
KnownBoundMethodType::FunctionTypeDunderGet(_)
1058910606
| KnownBoundMethodType::FunctionTypeDunderCall(_)
@@ -10595,6 +10612,7 @@ impl<'db> KnownBoundMethodType<'db> {
1059510612
| KnownBoundMethodType::ConstraintSetAlways
1059610613
| KnownBoundMethodType::ConstraintSetNever
1059710614
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10615+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1059810616
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
1059910617
) => ConstraintSet::from(false),
1060010618
}
@@ -10649,6 +10667,10 @@ impl<'db> KnownBoundMethodType<'db> {
1064910667
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints),
1065010668
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints),
1065110669
)
10670+
| (
10671+
KnownBoundMethodType::ConstraintSetSatisfies(left_constraints),
10672+
KnownBoundMethodType::ConstraintSetSatisfies(right_constraints),
10673+
)
1065210674
| (
1065310675
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(left_constraints),
1065410676
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(right_constraints),
@@ -10667,6 +10689,7 @@ impl<'db> KnownBoundMethodType<'db> {
1066710689
| KnownBoundMethodType::ConstraintSetAlways
1066810690
| KnownBoundMethodType::ConstraintSetNever
1066910691
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10692+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1067010693
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
1067110694
KnownBoundMethodType::FunctionTypeDunderGet(_)
1067210695
| KnownBoundMethodType::FunctionTypeDunderCall(_)
@@ -10678,6 +10701,7 @@ impl<'db> KnownBoundMethodType<'db> {
1067810701
| KnownBoundMethodType::ConstraintSetAlways
1067910702
| KnownBoundMethodType::ConstraintSetNever
1068010703
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10704+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1068110705
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
1068210706
) => ConstraintSet::from(false),
1068310707
}
@@ -10703,6 +10727,7 @@ impl<'db> KnownBoundMethodType<'db> {
1070310727
| KnownBoundMethodType::ConstraintSetAlways
1070410728
| KnownBoundMethodType::ConstraintSetNever
1070510729
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10730+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1070610731
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => self,
1070710732
}
1070810733
}
@@ -10720,6 +10745,7 @@ impl<'db> KnownBoundMethodType<'db> {
1072010745
| KnownBoundMethodType::ConstraintSetAlways
1072110746
| KnownBoundMethodType::ConstraintSetNever
1072210747
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
10748+
| KnownBoundMethodType::ConstraintSetSatisfies(_)
1072310749
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
1072410750
KnownClass::ConstraintSet
1072510751
}
@@ -10862,6 +10888,14 @@ impl<'db> KnownBoundMethodType<'db> {
1086210888
)))
1086310889
}
1086410890

10891+
KnownBoundMethodType::ConstraintSetSatisfies(_) => {
10892+
Either::Right(std::iter::once(Signature::new(
10893+
Parameters::new([Parameter::positional_only(Some(Name::new_static("other")))
10894+
.with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]),
10895+
Some(KnownClass::ConstraintSet.to_instance(db)),
10896+
)))
10897+
}
10898+
1086510899
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
1086610900
Either::Right(std::iter::once(Signature::new(
1086710901
Parameters::new([Parameter::keyword_only(Name::new_static("inferable"))

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,26 @@ impl<'db> Bindings<'db> {
11761176
));
11771177
}
11781178

1179+
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfies(
1180+
tracked,
1181+
)) => {
1182+
let [Some(other)] = overload.parameter_types() else {
1183+
continue;
1184+
};
1185+
let Type::KnownInstance(KnownInstanceType::ConstraintSet(other)) = other
1186+
else {
1187+
continue;
1188+
};
1189+
1190+
let result = tracked
1191+
.constraints(db)
1192+
.implies(db, || other.constraints(db));
1193+
let tracked = TrackedConstraintSet::new(db, result);
1194+
overload.set_return_type(Type::KnownInstance(
1195+
KnownInstanceType::ConstraintSet(tracked),
1196+
));
1197+
}
1198+
11791199
Type::KnownBoundMethod(
11801200
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked),
11811201
) => {

0 commit comments

Comments
 (0)