@@ -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
263307The 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
341385def _[T]() -> None :
@@ -350,13 +394,14 @@ def _[T]() -> None:
350394In 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
362407def _[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
446550The 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+ ```
0 commit comments