-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[ty] Add new "constraint implication" typing relation #21010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f27ee34
4172baa
73ec885
b4468d8
f3c699c
8954ae1
d923bec
ed1d05c
f86f271
9cf299b
f1074e4
73539e0
d58674c
f9e2d58
a4d0f67
e061f04
cd519b2
930f7a4
57ca6e1
54e7f81
b13db16
9e82637
38cbafe
e741e8d
a6db701
0a45929
6e3a561
89ab773
3c7950c
8a2573a
67a6c31
e17dd92
18f1d42
8779dae
14b636a
f42cf93
0395ae9
be9749c
362658b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| # Constraint implication | ||
|
|
||
| ```toml | ||
| [environment] | ||
| python-version = "3.12" | ||
| ``` | ||
|
|
||
| This file tests the _constraint implication_ relationship between types, aka `is_subtype_of_given`, | ||
| which tests whether one type is a [subtype][subtyping] of another _assuming that the constraints in | ||
| a particular constraint set hold_. | ||
|
|
||
| ## Concrete types | ||
|
|
||
| For concrete types, constraint implication is exactly the same as subtyping. (A concrete type is any | ||
| fully static type that is not a typevar. It can _contain_ a typevar, though — `list[T]` is | ||
| considered concrete.) | ||
|
|
||
| ```py | ||
| from ty_extensions import is_subtype_of, is_subtype_of_given, static_assert | ||
|
|
||
| def equivalent_to_other_relationships[T](): | ||
| static_assert(is_subtype_of(bool, int)) | ||
| static_assert(is_subtype_of_given(True, bool, int)) | ||
|
|
||
| static_assert(not is_subtype_of(bool, str)) | ||
| static_assert(not is_subtype_of_given(True, bool, str)) | ||
| ``` | ||
|
|
||
| Moreover, for concrete types, the answer does not depend on which constraint set we are considering. | ||
| `bool` is a subtype of `int` no matter what types any typevars are specialized to — and even if | ||
| there isn't a valid specialization for the typevars we are considering. | ||
|
|
||
| ```py | ||
| from typing import Never | ||
| from ty_extensions import range_constraint | ||
|
|
||
| def even_given_constraints[T](): | ||
| constraints = range_constraint(Never, T, int) | ||
| static_assert(is_subtype_of_given(constraints, bool, int)) | ||
| static_assert(not is_subtype_of_given(constraints, bool, str)) | ||
|
|
||
| def even_given_unsatisfiable_constraints(): | ||
| static_assert(is_subtype_of_given(False, bool, int)) | ||
| static_assert(not is_subtype_of_given(False, bool, str)) | ||
| ``` | ||
|
|
||
| ## Type variables | ||
|
|
||
| The interesting case is typevars. The other typing relationships (TODO: will) all "punt" on the | ||
| question when considering a typevar, by translating the desired relationship into a constraint set. | ||
|
Comment on lines
+49
to
+50
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This TODO is #20093 |
||
|
|
||
| ```py | ||
| from typing import Any | ||
| from ty_extensions import is_assignable_to, is_subtype_of | ||
|
|
||
| def assignability[T](): | ||
| # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ bool] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_assignable_to(T, bool)) | ||
| # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ int] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_assignable_to(T, int)) | ||
| # revealed: ty_extensions.ConstraintSet[always] | ||
| reveal_type(is_assignable_to(T, object)) | ||
|
|
||
| def subtyping[T](): | ||
| # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ bool] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(T, bool)) | ||
| # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ int] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(T, int)) | ||
| # revealed: ty_extensions.ConstraintSet[always] | ||
| reveal_type(is_subtype_of(T, object)) | ||
| ``` | ||
|
|
||
| When checking assignability with a dynamic type, we use the bottom and top materializations of the | ||
| lower and upper bounds, respectively. For subtyping, we use the top and bottom materializations. | ||
| (That is, assignability turns into a "permissive" constraint, and subtyping turns into a | ||
| "conservative" constraint.) | ||
|
|
||
| ```py | ||
| class Covariant[T]: | ||
| def get(self) -> T: | ||
| raise ValueError | ||
|
|
||
| class Contravariant[T]: | ||
| def set(self, value: T): | ||
| pass | ||
|
|
||
| def assignability[T](): | ||
| # aka [T@assignability ≤ object], which is always satisfiable | ||
| # revealed: ty_extensions.ConstraintSet[always] | ||
| reveal_type(is_assignable_to(T, Any)) | ||
|
|
||
| # aka [Never ≤ T@assignability], which is always satisfiable | ||
| # revealed: ty_extensions.ConstraintSet[always] | ||
| reveal_type(is_assignable_to(Any, T)) | ||
|
|
||
| # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Covariant[object]] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_assignable_to(T, Covariant[Any])) | ||
| # TODO: revealed: ty_extensions.ConstraintSet[Covariant[Never] ≤ T@assignability] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_assignable_to(Covariant[Any], T)) | ||
|
|
||
| # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Contravariant[Never]] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_assignable_to(T, Contravariant[Any])) | ||
| # TODO: revealed: ty_extensions.ConstraintSet[Contravariant[object] ≤ T@assignability] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_assignable_to(Contravariant[Any], T)) | ||
|
|
||
| def subtyping[T](): | ||
| # aka [T@assignability ≤ object], which is always satisfiable | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(T, Any)) | ||
|
|
||
| # aka [Never ≤ T@assignability], which is always satisfiable | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(Any, T)) | ||
|
|
||
| # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Covariant[Never]] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(T, Covariant[Any])) | ||
| # TODO: revealed: ty_extensions.ConstraintSet[Covariant[object] ≤ T@subtyping] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(Covariant[Any], T)) | ||
|
|
||
| # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Contravariant[object]] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(T, Contravariant[Any])) | ||
| # TODO: revealed: ty_extensions.ConstraintSet[Contravariant[Never] ≤ T@subtyping] | ||
| # revealed: ty_extensions.ConstraintSet[never] | ||
| reveal_type(is_subtype_of(Contravariant[Any], T)) | ||
| ``` | ||
|
|
||
| At some point, though, we need to resolve a constraint set; at that point, we can no longer punt on | ||
| the question. Unlike with concrete types, the answer will depend on the constraint set that we are | ||
| considering. | ||
|
|
||
| ```py | ||
| from typing import Never | ||
| from ty_extensions import is_subtype_of_given, range_constraint, static_assert | ||
|
|
||
| def given_constraints[T](): | ||
| static_assert(not is_subtype_of_given(True, T, int)) | ||
| static_assert(not is_subtype_of_given(True, T, bool)) | ||
| static_assert(not is_subtype_of_given(True, T, str)) | ||
|
|
||
| # These are vacuously true; false implies anything | ||
| static_assert(is_subtype_of_given(False, T, int)) | ||
| static_assert(is_subtype_of_given(False, T, bool)) | ||
| static_assert(is_subtype_of_given(False, T, str)) | ||
|
|
||
| given_int = range_constraint(Never, T, int) | ||
| static_assert(is_subtype_of_given(given_int, T, int)) | ||
| static_assert(not is_subtype_of_given(given_int, T, bool)) | ||
| static_assert(not is_subtype_of_given(given_int, T, str)) | ||
|
|
||
| given_bool = range_constraint(Never, T, bool) | ||
| static_assert(is_subtype_of_given(given_bool, T, int)) | ||
| static_assert(is_subtype_of_given(given_bool, T, bool)) | ||
| static_assert(not is_subtype_of_given(given_bool, T, str)) | ||
|
|
||
| given_both = given_bool & given_int | ||
| static_assert(is_subtype_of_given(given_both, T, int)) | ||
| static_assert(is_subtype_of_given(given_both, T, bool)) | ||
| static_assert(not is_subtype_of_given(given_both, T, str)) | ||
|
|
||
| given_str = range_constraint(Never, T, str) | ||
| static_assert(not is_subtype_of_given(given_str, T, int)) | ||
| static_assert(not is_subtype_of_given(given_str, T, bool)) | ||
| static_assert(is_subtype_of_given(given_str, T, str)) | ||
| ``` | ||
|
|
||
| This might require propagating constraints from other typevars. | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be involved enough that I'm tackling it separately, in #21068 |
||
|
|
||
| ```py | ||
| def mutually_constrained[T, U](): | ||
| # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. | ||
| given_int = range_constraint(U, T, U) & range_constraint(Never, U, int) | ||
| # TODO: no static-assert-error | ||
| # error: [static-assert-error] | ||
| static_assert(is_subtype_of_given(given_int, T, int)) | ||
| static_assert(not is_subtype_of_given(given_int, T, bool)) | ||
| static_assert(not is_subtype_of_given(given_int, T, str)) | ||
|
|
||
| # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. | ||
| given_int = range_constraint(Never, T, U) & range_constraint(Never, U, int) | ||
| # TODO: no static-assert-error | ||
| # error: [static-assert-error] | ||
| static_assert(is_subtype_of_given(given_int, T, int)) | ||
| static_assert(not is_subtype_of_given(given_int, T, bool)) | ||
| static_assert(not is_subtype_of_given(given_int, T, str)) | ||
| ``` | ||
|
|
||
| [subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had originally written this as a new |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1724,7 +1724,7 @@ impl<'db> Type<'db> { | |
| // since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive. | ||
| (Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar)) | ||
| if !lhs_bound_typevar.is_inferable(db, inferable) | ||
| && lhs_bound_typevar.identity(db) == rhs_bound_typevar.identity(db) => | ||
| && lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) => | ||
| { | ||
| ConstraintSet::from(true) | ||
| } | ||
|
|
@@ -2621,7 +2621,7 @@ impl<'db> Type<'db> { | |
| // constraints, which are handled below. | ||
| (Type::TypeVar(self_bound_typevar), Type::TypeVar(other_bound_typevar)) | ||
| if !self_bound_typevar.is_inferable(db, inferable) | ||
| && self_bound_typevar.identity(db) == other_bound_typevar.identity(db) => | ||
| && self_bound_typevar.is_same_typevar_as(db, other_bound_typevar) => | ||
| { | ||
| ConstraintSet::from(false) | ||
| } | ||
|
|
@@ -4833,6 +4833,30 @@ impl<'db> Type<'db> { | |
| ) | ||
| .into(), | ||
|
|
||
| Some(KnownFunction::IsSubtypeOfGiven) => Binding::single( | ||
| self, | ||
| Signature::new( | ||
| Parameters::new([ | ||
| Parameter::positional_only(Some(Name::new_static("constraints"))) | ||
| .with_annotated_type(UnionType::from_elements( | ||
| db, | ||
| [ | ||
| KnownClass::Bool.to_instance(db), | ||
| KnownClass::ConstraintSet.to_instance(db), | ||
| ], | ||
| )), | ||
| Parameter::positional_only(Some(Name::new_static("ty"))) | ||
| .type_form() | ||
| .with_annotated_type(Type::any()), | ||
| Parameter::positional_only(Some(Name::new_static("of"))) | ||
| .type_form() | ||
| .with_annotated_type(Type::any()), | ||
| ]), | ||
| Some(KnownClass::ConstraintSet.to_instance(db)), | ||
| ), | ||
| ) | ||
| .into(), | ||
|
|
||
| Some(KnownFunction::RangeConstraint | KnownFunction::NegatedRangeConstraint) => { | ||
| Binding::single( | ||
| self, | ||
|
|
@@ -8534,7 +8558,6 @@ pub struct BoundTypeVarIdentity<'db> { | |
| /// A type variable that has been bound to a generic context, and which can be specialized to a | ||
| /// concrete type. | ||
| #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] | ||
| #[derive(PartialOrd, Ord)] | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should always be comparing a typevar's identity, and this makes it harder to accidentally compare specific instances of a typevar. |
||
| pub struct BoundTypeVarInstance<'db> { | ||
| pub typevar: TypeVarInstance<'db>, | ||
| binding_context: BindingContext<'db>, | ||
|
|
@@ -8555,6 +8578,12 @@ impl<'db> BoundTypeVarInstance<'db> { | |
| } | ||
| } | ||
|
|
||
| /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. | ||
| /// differences in their bounds or constraints due to materialization. | ||
| pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { | ||
| self.identity(db) == other.identity(db) | ||
| } | ||
|
|
||
| /// Create a new PEP 695 type variable that can be used in signatures | ||
| /// of synthetic generic functions. | ||
| pub(crate) fn synthetic( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
is_subtype_of(T, V)guaranteed to always be the same asis_subtype_of(True, T, V). If so, maybe that's something you could add to the documentation ofis_subtype_of_givenThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great catch! I was in the middle of writing that this would not be true when comparing typevars — but you're right, and it is true even then:
This also made me realize that I don't need to (and shouldn't) project away the other typevars like I was doing.