add quantification test cases

This commit is contained in:
Douglas Creager
2025-12-17 17:06:20 -05:00
parent b051943504
commit f22890bd88
2 changed files with 314 additions and 10 deletions

View File

@@ -416,19 +416,44 @@ def mutually_bound[T: Base, U]():
## Nested typevars
A typevar's constraint can _mention_ another typevar without _constraining_ it. In this example, `U`
must be specialized to `list[T]`, but it cannot affect what `T` is specialized to.
The specialization of one typevar can affect the specialization of another, even if it is not a
"top-level" type in the bounds. (That is, if it appears as inside the specialization of a generic
class.)
```py
from typing import Never
from ty_extensions import ConstraintSet, generic_context
def mentions[T, U]():
# (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions])
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T])
# TODO: revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = Unknown]
reveal_type(generic_context(mentions).specialize_constrained(constraints))
class Covariant[T]:
def get(self) -> T:
raise NotImplementedError
class Contravariant[T]:
def receive(self, input: T): ...
class Invariant[T]:
mutable_attribute: T
def mentions_covariant[T, U]():
# (T@mentions_covariant ≤ int) ∧ (U@mentions_covariant ≤ Covariant[T@mentions_covariant])
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Covariant[T])
# TODO: revealed: ty_extensions.Specialization[T@mentions_covariant = int, U@mentions_covariant = Covariant[int]]
# revealed: ty_extensions.Specialization[T@mentions_covariant = int, U@mentions_covariant = Unknown]
reveal_type(generic_context(mentions_covariant).specialize_constrained(constraints))
def mentions_contravariant[T, U]():
# (T@mentions_contravariant ≤ int) ∧ (Contravariant[T@mentions_contravariant] ≤ U@mentions_contravariant)
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Contravariant[T], U, object)
# TODO: revealed: ty_extensions.Specialization[T@mentions_contravariant = int, U@mentions_contravariant = Contravariant[int]]
# revealed: ty_extensions.Specialization[T@mentions_contravariant = int, U@mentions_contravariant = Unknown]
reveal_type(generic_context(mentions_contravariant).specialize_constrained(constraints))
def mentions_invariant[T, U]():
# (T@mentions_invariant ≤ int) ∧ (U@mentions_invariant = Invariant[T@mentions_invariant])
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Invariant[T], U, Invariant[T])
# TODO: revealed: ty_extensions.Specialization[T@mentions_invariant = int, U@mentions_invariant = Invariant[int]]
# revealed: ty_extensions.Specialization[T@mentions_invariant = int, U@mentions_invariant = Unknown]
reveal_type(generic_context(mentions_invariant).specialize_constrained(constraints))
```
If the constraint set contains mutually recursive bounds, specialization inference will not
@@ -437,8 +462,8 @@ this case.
```py
def divergent[T, U]():
# (T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent]))
constraints = ConstraintSet.range(list[U], T, list[U]) & ConstraintSet.range(list[T], U, list[T])
# (T@divergent = Invariant[U@divergent]) ∧ (U@divergent = Invariant[T@divergent]))
constraints = ConstraintSet.range(Invariant[U], T, Invariant[U]) & ConstraintSet.range(Invariant[T], U, Invariant[T])
# revealed: None
reveal_type(generic_context(divergent).specialize_constrained(constraints))
```

View File

@@ -0,0 +1,279 @@
# Constraint set quantification
```toml
[environment]
python-version = "3.12"
```
We can _existentially quantify_ a constraint set over a type variable. The result is a copy of the
constraint set that only mentions the requested typevar. All constraints mentioning any other
typevars are removed. Importantly, they are removed "safely", with their constraints propagated
through to the remaining constraints as needed.
## Keeping a single typevar
If a constraint set only mentions a single typevar, and we keep that typevar when quantifying, the
result is unchanged.
```py
from ty_extensions import ConstraintSet, static_assert
class Base: ...
class Sub(Base): ...
def keep_single[T]():
constraints = ConstraintSet.always()
quantified = ConstraintSet.always()
static_assert(constraints.retain_one(T) == quantified)
constraints = ConstraintSet.never()
quantified = ConstraintSet.never()
static_assert(constraints.retain_one(T) == quantified)
constraints = ConstraintSet.range(Sub, T, Base)
quantified = ConstraintSet.range(Sub, T, Base)
static_assert(constraints.retain_one(T) == quantified)
```
## Removing a single typevar
If a constraint set only mentions a single typevar, and we remove that typevar when quantifying,
the result is usually "always". The only exception is if the original constraint set has no
solution. In that case, the result is also unsatisfiable.
```py
from ty_extensions import ConstraintSet, static_assert
class Base: ...
class Sub(Base): ...
def remove_single[T]():
constraints = ConstraintSet.always()
quantified = ConstraintSet.always()
static_assert(constraints.exists(T) == quantified)
constraints = ConstraintSet.never()
quantified = ConstraintSet.never()
static_assert(constraints.exists(T) == quantified)
constraints = ConstraintSet.range(Sub, T, Base)
quantified = ConstraintSet.always()
static_assert(constraints.exists(T) == quantified)
```
This also holds when the constraint set contains multiple typevars. In the cases below, we are
keeping `U`, and the constraints on `T` do not ever affect what `U` can specialize to — `U` can
specialize to anything (unless the original constraint set is unsatisfiable).
```py
from ty_extensions import ConstraintSet, static_assert
class Base: ...
class Sub(Base): ...
def remove_other[T, U]():
constraints = ConstraintSet.always()
quantified = ConstraintSet.always()
static_assert(constraints.retain_one(U) == quantified)
constraints = ConstraintSet.never()
quantified = ConstraintSet.never()
static_assert(constraints.retain_one(U) == quantified)
constraints = ConstraintSet.range(Sub, T, Base)
quantified = ConstraintSet.always()
static_assert(constraints.retain_one(U) == quantified)
```
## Transitivity
When a constraint set mentions two typevars, and compares them directly, then we can use
transitivity to propagate the other constraints when quantifying.
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
def transitivity[T, U]():
# (Base ≤ T) ∧ (T ≤ U) → (Base ≤ U)
constraints = ConstraintSet.range(Base, T, object) & ConstraintSet.range(T, U, object)
quantified = ConstraintSet.range(Base, U, object)
static_assert(constraints.exists(T) == quantified)
# (Base ≤ T ≤ Super) ∧ (T ≤ U) → (Base ≤ U)
constraints = ConstraintSet.range(Base, T, Super) & ConstraintSet.range(T, U, object)
quantified = ConstraintSet.range(Base, U, object)
static_assert(constraints.exists(T) == quantified)
# (T ≤ Base) ∧ (U ≤ T) → (U ≤ Base)
constraints = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, T)
quantified = ConstraintSet.range(Never, U, Base)
static_assert(constraints.exists(T) == quantified)
# (Sub ≤ T ≤ Base) ∧ (U ≤ T) → (U ≤ Base)
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Never, U, T)
quantified = ConstraintSet.range(Never, U, Base)
static_assert(constraints.exists(T) == quantified)
```
## Covariant transitivity
The same applies when one of the typevars is used covariantly in a bound of the other typevar.
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Covariant[T]:
def get(self) -> T:
raise NotImplementedError
def covariant_transitivity[T, U]():
# (Base ≤ T) ∧ (Covariant[T] ≤ U) → (Covariant[Base] ≤ U)
constraints = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Covariant[T], U, object)
quantified = ConstraintSet.range(Covariant[Base], U, object)
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (Base ≤ T ≤ Super) ∧ (Covariant[T] ≤ U) → (Covariant[Base] ≤ U)
constraints = ConstraintSet.range(Base, T, Super) & ConstraintSet.range(Covariant[T], U, object)
quantified = ConstraintSet.range(Covariant[Base], U, object)
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (T ≤ Base) ∧ (U ≤ Covariant[T]) → (U ≤ Covariant[Base])
constraints = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Covariant[T])
quantified = ConstraintSet.range(Never, U, Covariant[Base])
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (Sub ≤ T ≤ Base) ∧ (U ≤ Covariant[T]) → (U ≤ Covariant[Base])
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Never, U, Covariant[T])
quantified = ConstraintSet.range(Never, U, Covariant[Base])
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
```
## Contravariant transitivity
Similar rules apply, but in reverse, when one of the typevars is used contravariantly in a bound of
the other typevar.
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Contravariant[T]:
def receive(self, input: T): ...
def contravariant_transitivity[T, U]():
# (Base ≤ T) ∧ (U ≤ Contravariant[T]) → (U ≤ Contravariant[Base])
constraints = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Never, U, Contravariant[T])
quantified = ConstraintSet.range(Never, U, Contravariant[Base])
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (Base ≤ T ≤ Super) ∧ (U ≤ Contravariant[T]) → (U ≤ Contravariant[Base])
constraints = ConstraintSet.range(Base, T, Super) & ConstraintSet.range(Never, U, Contravariant[T])
quantified = ConstraintSet.range(Never, U, Contravariant[Base])
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (T ≤ Base) ∧ (Contravariant[T] ≤ U) → (Contravariant[Base] ≤ U)
constraints = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Contravariant[T], U, object)
quantified = ConstraintSet.range(Contravariant[Base], U, object)
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (Sub ≤ T ≤ Base) ∧ (Contravariant[T] ≤ U) → (Contravariant[Base] ≤ U)
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Contravariant[T], U, object)
quantified = ConstraintSet.range(Contravariant[Base], U, object)
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
```
## Invariant transitivity involving equality constraints
Invariant uses of a typevar are more subtle. The simplest case is when there is an _equality_
constraint on the invariant typevar. In that case, we know precisely which specialization is
required.
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
class Base: ...
class Invariant[T]:
mutable_attribute: T
def invariant_equality_transitivity[T, U]():
# (T = Base) ∧ (U ≤ Invariant[T]) → (U ≤ Invariant[Base])
constraints = ConstraintSet.range(Base, T, Base) & ConstraintSet.range(Never, U, Invariant[T])
quantified = ConstraintSet.range(Never, U, Invariant[Base])
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (T = Base) ∧ (Invariant[T] ≤ U) → (Invariant[Base] ≤ U)
constraints = ConstraintSet.range(Base, T, Base) & ConstraintSet.range(Invariant[T], U, object)
quantified = ConstraintSet.range(Invariant[Base], U, object)
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
```
## Invariant transitivity involving range constraints
When there is a _range_ constraint on the invariant typevar, we still have to retain information
about which range of types the quantified-away typevar can specialize to, since this affects which
types the remaining typevar can specialize to, and invariant typevars are not monotonic like
covariant and contravariant typevars.
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
class Base: ...
class Sub(Base): ...
class Invariant[T]:
mutable_attribute: T
def invariant_range_transitivity[T, U]():
# (Sub ≤ T ≤ Base) ∧ (U ≤ Invariant[T]) → (U ≤ Invariant[Exists[Sub, Base]])
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Never, U, Invariant[T])
# TODO: The existential that we need doesn't exist yet.
quantified = ConstraintSet.never()
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
# (Sub ≤ T ≤ Base) ∧ (Invariant[T] ≤ U) → (Invariant[Exists[Sub, Base]] ≤ U)
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Invariant[T], U, object)
# TODO: The existential that we need doesn't exist yet.
quantified = ConstraintSet.never()
# TODO: no error
# error: [static-assert-error]
static_assert(constraints.exists(T) == quantified)
```