mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[ty] Clarify behavior of constraint sets for gradual upper bounds and constraints (#21287)
When checking whether a constraint set is satisfied, if a typevar has a non-fully-static upper bound or constraint, we are free to choose any materialization that makes the check succeed. In non-inferable positions, we have to show that the constraint set is satisfied for all valid specializations, so it's best to choose the most restrictive materialization, since that minimizes the set of valid specializations that have to pass. In inferable positions, we only have to show that the constraint set is satisfied for _some_ valid specializations, so it's best to choose the most permissive materialization, since that maximizes our chances of finding a specialization that passes.
This commit is contained in:
parent
276f1d0d88
commit
faae72b836
2 changed files with 388 additions and 35 deletions
|
|
@ -141,6 +141,97 @@ def bounded[T: Base]():
|
|||
static_assert(not constraints.satisfied_by_all_typevars())
|
||||
```
|
||||
|
||||
If the upper bound is a gradual type, we are free to choose any materialization of the upper bound
|
||||
that makes the test succeed. In non-inferable positions, it is most helpful to choose the bottom
|
||||
materialization as the upper bound. That is the most restrictive possible choice, which minimizes
|
||||
the number of valid specializations that must satisfy the constraint set. In inferable positions,
|
||||
the opposite is true: it is most helpful to choose the top materialization. That is the most
|
||||
permissive possible choice, which maximizes the number of valid specializations that might satisfy
|
||||
the constraint set.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def bounded_by_gradual[T: Any]():
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
|
||||
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Base as the materialization for the upper bound, then (T = Base) is a valid
|
||||
# specialization, which satisfies (T ≤ Base).
|
||||
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# We are free to choose any materialization of the upper bound, and only have to show that the
|
||||
# constraint set holds for that one materialization. Having chosen one materialization, we then
|
||||
# have to show that the constraint set holds for all valid specializations of that
|
||||
# materialization. If we choose Never as the materialization, then all valid specializations
|
||||
# must satisfy (T ≤ Never). That means there is only one valid specialization, (T = Never),
|
||||
# which satisfies (T ≤ Base).
|
||||
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Unrelated as the materialization, then (T = Unrelated) is a valid specialization,
|
||||
# which satisfies (T ≤ Unrelated).
|
||||
constraints = ConstraintSet.range(Never, T, Unrelated)
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Never as the materialization, then (T = Never) is the only valid specialization,
|
||||
# which satisfies (T ≤ Unrelated).
|
||||
static_assert(constraints.satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Unrelated as the materialization, then (T = Unrelated) is a valid specialization,
|
||||
# which satisfies (T ≤ Unrelated ∧ T ≠ Never).
|
||||
constraints = constraints & ~ConstraintSet.range(Never, T, Never)
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# There is no upper bound that we can choose to satisfy this constraint set in non-inferable
|
||||
# position. (T = Never) will be a valid assignment no matter what, and that does not satisfy
|
||||
# (T ≤ Unrelated ∧ T ≠ Never).
|
||||
static_assert(not constraints.satisfied_by_all_typevars())
|
||||
```
|
||||
|
||||
When the upper bound is a more complex gradual type, we are still free to choose any materialization
|
||||
that causes the check to succeed, and we will still choose the bottom materialization in
|
||||
non-inferable position, and the top materialization in inferable position. The variance of the
|
||||
typevar does not affect whether there is a materialization we can choose. Below, we test the most
|
||||
restrictive variance (i.e., invariance), but we get the same results for other variances as well.
|
||||
|
||||
```py
|
||||
def bounded_by_gradual[T: list[Any]]():
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
|
||||
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Base] as the materialization of the upper bound, then (T = list[Base]) is a
|
||||
# valid specialization, which satisfies (T ≤ list[Base]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Base as the materialization, then all valid specializations must satisfy
|
||||
# (T ≤ list[Base]).
|
||||
# We are free to choose any materialization of the upper bound, and only have to show that the
|
||||
# constraint set holds for that one materialization. Having chosen one materialization, we then
|
||||
# have to show that the constraint set holds for all valid specializations of that
|
||||
# materialization. If we choose list[Base] as the materialization, then all valid specializations
|
||||
# must satisfy (T ≤ list[Base]), which is exactly the constraint set that we need to satisfy.
|
||||
static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Unrelated as the materialization, then (T = list[Unrelated]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Unrelated]).
|
||||
constraints = ConstraintSet.range(Never, T, list[Unrelated])
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Unrelated as the materialization, then all valid specializations must satisfy
|
||||
# (T ≤ list[Unrelated]).
|
||||
static_assert(constraints.satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Unrelated as the materialization, then (T = list[Unrelated]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never).
|
||||
constraints = constraints & ~ConstraintSet.range(Never, T, Never)
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# There is no upper bound that we can choose to satisfy this constraint set in non-inferable
|
||||
# position. (T = Never) will be a valid assignment no matter what, and that does not satisfy
|
||||
# (T ≤ list[Unrelated] ∧ T ≠ Never).
|
||||
static_assert(not constraints.satisfied_by_all_typevars())
|
||||
```
|
||||
|
||||
## Constrained typevar
|
||||
|
||||
If a typevar has constraints, then it must specialize to one of those specific types. (Not to a
|
||||
|
|
@ -218,3 +309,174 @@ def constrained[T: (Base, Unrelated)]():
|
|||
# (T = Base) is a valid specialization, which does not satisfy (T = Sub ∨ T = Unrelated).
|
||||
static_assert(not constraints.satisfied_by_all_typevars())
|
||||
```
|
||||
|
||||
If any of the constraints is a gradual type, we are free to choose any materialization of that
|
||||
constraint that makes the test succeed. In non-inferable positions, it is most helpful to choose the
|
||||
bottom materialization as the constraint. That is the most restrictive possible choice, which
|
||||
minimizes the number of valid specializations that must satisfy the constraint set. In inferable
|
||||
positions, the opposite is true: it is most helpful to choose the top materialization. That is the
|
||||
most permissive possible choice, which maximizes the number of valid specializations that might
|
||||
satisfy the constraint set.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def constrained_by_gradual[T: (Base, Any)]():
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
|
||||
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Unrelated as the materialization of the gradual constraint, then (T = Unrelated)
|
||||
# is a valid specialization, which satisfies (T ≤ Unrelated).
|
||||
static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = Base) is a valid specialization, which does
|
||||
# not satisfy (T ≤ Unrelated).
|
||||
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Super as the materialization, then (T = Super) is a valid specialization, which
|
||||
# satisfies (T ≤ Super).
|
||||
static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Never as the materialization, then (T = Base) and (T = Never) are the only valid
|
||||
# specializations, both of which satisfy (T ≤ Super).
|
||||
static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Base as the materialization, then (T = Base) is a valid specialization, which
|
||||
# satisfies (T ≤ Base).
|
||||
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Never as the materialization, then (T = Base) and (T = Never) are the only valid
|
||||
# specializations, both of which satisfy (T ≤ Base).
|
||||
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars())
|
||||
|
||||
def constrained_by_two_gradual[T: (Any, Any)]():
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
|
||||
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Unrelated as the materialization of either constraint, then (T = Unrelated) is a
|
||||
# valid specialization, which satisfies (T ≤ Unrelated).
|
||||
static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Unrelated as the materialization of both constraints, then (T = Unrelated) is the
|
||||
# only valid specialization, which satisfies (T ≤ Unrelated).
|
||||
static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose Base as the materialization of either constraint, then (T = Base) is a valid
|
||||
# specialization, which satisfies (T ≤ Base).
|
||||
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Never as the materialization of both constraints, then (T = Never) is the only
|
||||
# valid specialization, which satisfies (T ≤ Base).
|
||||
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars())
|
||||
```
|
||||
|
||||
When a constraint is a more complex gradual type, we are still free to choose any materialization
|
||||
that causes the check to succeed, and we will still choose the bottom materialization in
|
||||
non-inferable position, and the top materialization in inferable position. The variance of the
|
||||
typevar does not affect whether there is a materialization we can choose. Below, we test the most
|
||||
restrictive variance (i.e., invariance), but we get the same results for other variances as well.
|
||||
|
||||
```py
|
||||
def constrained_by_gradual[T: (list[Base], list[Any])]():
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
|
||||
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
|
||||
|
||||
# No matter which materialization we choose, every valid specialization will be of the form
|
||||
# (T = list[X]). Because Unrelated is final, it is disjoint from all lists. There is therefore
|
||||
# no materialization or specialization that satisfies (T ≤ Unrelated).
|
||||
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Super] as the materialization, then (T = list[Super]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Super]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = list[Base]) is a valid specialization, which
|
||||
# does not satisfy (T ≤ list[Super]).
|
||||
static_assert(not ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Base] as the materialization, then (T = list[Base]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Base]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose list[Base] as the materialization, then all valid specializations must satisfy
|
||||
# (T ≤ list[Base]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Sub] as the materialization, then (T = list[Sub]) is a valid specialization,
|
||||
# which # satisfies (T ≤ list[Sub]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = list[Base]) is a valid specialization, which
|
||||
# does not satisfy (T ≤ list[Sub]).
|
||||
static_assert(not ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Unrelated]).
|
||||
constraints = ConstraintSet.range(Never, T, list[Unrelated])
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = list[Base]) is a valid specialization, which
|
||||
# does not satisfy (T ≤ list[Unrelated]).
|
||||
static_assert(not constraints.satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never).
|
||||
constraints = constraints & ~ConstraintSet.range(Never, T, Never)
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# There is no materialization that we can choose to satisfy this constraint set in non-inferable
|
||||
# position. (T = Never) will be a valid assignment no matter what, and that does not satisfy
|
||||
# (T ≤ list[Unrelated] ∧ T ≠ Never).
|
||||
static_assert(not constraints.satisfied_by_all_typevars())
|
||||
|
||||
def constrained_by_two_gradual[T: (list[Any], list[Any])]():
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
|
||||
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
|
||||
|
||||
# No matter which materialization we choose, every valid specialization will be of the form
|
||||
# (T = list[X]). Because Unrelated is final, it is disjoint from all lists. There is therefore
|
||||
# no materialization or specialization that satisfies (T ≤ Unrelated).
|
||||
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Super] as the materialization, then (T = list[Super]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Super]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = list[Base]) is a valid specialization, which
|
||||
# does not satisfy (T ≤ list[Super]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Base] as the materialization, then (T = list[Base]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Base]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# If we choose Base as the materialization, then all valid specializations must satisfy
|
||||
# (T ≤ list[Base]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Sub] as the materialization, then (T = list[Sub]) is a valid specialization,
|
||||
# which satisfies (T ≤ list[Sub]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = list[Base]) is a valid specialization, which
|
||||
# does not satisfy (T ≤ list[Sub]).
|
||||
static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Unrelated]).
|
||||
constraints = ConstraintSet.range(Never, T, list[Unrelated])
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# No matter which materialization we choose, (T = list[Base]) is a valid specialization, which
|
||||
# does not satisfy (T ≤ list[Unrelated]).
|
||||
static_assert(constraints.satisfied_by_all_typevars())
|
||||
|
||||
# If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid
|
||||
# specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never).
|
||||
constraints = constraints & ~ConstraintSet.range(Never, T, Never)
|
||||
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
|
||||
# There is no constraint that we can choose to satisfy this constraint set in non-inferable
|
||||
# position. (T = Never) will be a valid assignment no matter what, and that does not satisfy
|
||||
# (T ≤ list[Unrelated] ∧ T ≠ Never).
|
||||
static_assert(constraints.satisfied_by_all_typevars())
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue