-
Notifications
You must be signed in to change notification settings - Fork 2.4k
[ty] Respect type variables in top/bottom materializations of invariant generics #28072
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
base: main
Are you sure you want to change the base?
Changes from all commits
26fa5c2
55aa8f4
37ec0a8
a30f83a
a48e5bd
06d1fa6
413d4dc
26b2c0f
6bfe471
98e7ae3
88059dd
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 |
|---|---|---|
|
|
@@ -1186,6 +1186,63 @@ class Both(Left, Right): ... | |
| static_assert(not is_disjoint_from(Left, Right)) | ||
| ``` | ||
|
|
||
| ### Nested type variables in invariant arguments | ||
|
|
||
| An invariant argument can contain a type variable and still be incompatible with another argument. | ||
| For example, `list[T]` cannot equal `int`, regardless of the specialization of `T`. | ||
|
|
||
| ```toml | ||
| [environment] | ||
| python-version = "3.12" | ||
| ``` | ||
|
|
||
| ```py | ||
| from typing import Never | ||
| from ty_extensions import static_assert | ||
| from ty_extensions._internal import is_disjoint_from | ||
|
|
||
| def incompatible[T](): | ||
| static_assert(is_disjoint_from(list[list[T]], list[int])) | ||
| static_assert(is_disjoint_from(list[int], list[list[T]])) | ||
| static_assert(is_disjoint_from(list[tuple[T, int]], list[tuple[T, str]])) | ||
| static_assert(is_disjoint_from(list[tuple[T, str]], list[tuple[T, int]])) | ||
| ``` | ||
|
|
||
| When the surrounding structure matches, the arguments can instead be equal for some specialization. | ||
| Aliases preserve that possibility, including aliases nested inside the argument. | ||
|
|
||
| ```py | ||
| type Id[T] = T | ||
|
|
||
| def compatible[T](): | ||
| static_assert(not is_disjoint_from(list[list[T]], list[list[int]])) | ||
| static_assert(not is_disjoint_from(list[list[Id[T]]], list[list[int]])) | ||
| static_assert(not is_disjoint_from(list[list[T]], list[list[Never]])) | ||
|
Comment on lines
+1218
to
+1220
Contributor
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. these fail on |
||
| ``` | ||
|
|
||
| An upper bound can rule out equality even when the surrounding structure matches. A type variable | ||
| bounded by `str` cannot specialize to `int`, but it can specialize to `str` or `Never`. | ||
|
|
||
| ```py | ||
| def bounded[T: str](): | ||
| static_assert(is_disjoint_from(list[list[T]], list[list[int]])) | ||
| static_assert(is_disjoint_from(list[list[int]], list[list[T]])) | ||
| static_assert(not is_disjoint_from(list[list[T]], list[list[str]])) | ||
| static_assert(not is_disjoint_from(list[list[T]], list[list[Never]])) | ||
|
Comment on lines
+1230
to
+1231
Contributor
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. these fail on |
||
| ``` | ||
|
|
||
| A constrained type variable can only specialize to one of its constraints. Neither `int` nor `Never` | ||
| is a valid specialization, while matching either `str` or `bytes` preserves a possible overlap. | ||
|
|
||
| ```py | ||
| def constrained[T: (str, bytes)](): | ||
| static_assert(is_disjoint_from(list[list[T]], list[list[int]])) | ||
| static_assert(is_disjoint_from(list[list[int]], list[list[T]])) | ||
| static_assert(is_disjoint_from(list[list[T]], list[list[Never]])) | ||
| static_assert(not is_disjoint_from(list[list[T]], list[list[str]])) | ||
| static_assert(not is_disjoint_from(list[list[T]], list[list[bytes]])) | ||
|
Comment on lines
+1242
to
+1243
Contributor
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. these fail on |
||
| ``` | ||
|
|
||
| ### NewTypes and overlapping types | ||
|
|
||
| A `NewType` overlaps with any nominal or structural type that overlaps its concrete base. This | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -279,6 +279,37 @@ def takes_objects(*args: object, **kwargs: object) -> object: | |
| static_assert(not is_subtype_of(TopCallable, RegularCallableTypeOf[takes_objects])) | ||
| ``` | ||
|
|
||
| ## `ParamSpec` specializations | ||
|
|
||
| For a class invariant in a `ParamSpec`, every fixed specialization lies between the bottom and top | ||
| materializations of its `...` specialization. This holds for both subtyping and assignability. The | ||
| reverse relations do not hold for an arbitrary fixed specialization. | ||
|
|
||
| ```toml | ||
| [environment] | ||
| python-version = "3.12" | ||
| ``` | ||
|
|
||
| ```py | ||
| from typing import Callable | ||
| from ty_extensions import Bottom, Top, static_assert | ||
| from ty_extensions._internal import is_assignable_to, is_subtype_of | ||
|
|
||
| class Box[**P]: | ||
| callback: Callable[P, None] | ||
|
|
||
| def _[**P](): | ||
| static_assert(is_subtype_of(Box[P], Top[Box[...]])) | ||
| static_assert(is_subtype_of(Bottom[Box[...]], Box[P])) | ||
| static_assert(not is_subtype_of(Top[Box[...]], Box[P])) | ||
| static_assert(not is_subtype_of(Box[P], Bottom[Box[...]])) | ||
|
Comment on lines
+304
to
+305
Contributor
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. these two fail on |
||
|
|
||
| static_assert(is_assignable_to(Box[P], Top[Box[...]])) | ||
| static_assert(is_assignable_to(Bottom[Box[...]], Box[P])) | ||
| static_assert(not is_assignable_to(Top[Box[...]], Box[P])) | ||
| static_assert(not is_assignable_to(Box[P], Bottom[Box[...]])) | ||
|
Comment on lines
+309
to
+310
Contributor
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. these two fail on |
||
| ``` | ||
|
|
||
| ## Tuple | ||
|
|
||
| All positions in a tuple are covariant. | ||
|
|
@@ -1391,6 +1422,11 @@ def generic_recursive_materialization(value: Top[Covariant[GenericRecursive[int] | |
|
|
||
| ## Subtyping | ||
|
|
||
| ```toml | ||
| [environment] | ||
| python-version = "3.12" | ||
| ``` | ||
|
|
||
| Any `list[T]` is a subtype of `Top[list[Any]]`, but with more restrictive gradual types, not all | ||
| other specializations are subtypes. | ||
|
|
||
|
|
@@ -1463,6 +1499,24 @@ static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[bool | Any] | |
| static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[Any]])) | ||
| ``` | ||
|
|
||
| An unresolved type variable does not necessarily satisfy a materialization's bounds. Conversely, | ||
| `Top[list[Unknown]]` includes specializations that do not match an arbitrary fixed `T`. | ||
|
|
||
| ```pyi | ||
| from ty_extensions._internal import Unknown | ||
|
|
||
| def unresolved[T](): | ||
| static_assert(not is_subtype_of(list[T], Top[list[int & Any]])) | ||
| static_assert(not is_subtype_of(Top[list[Unknown]], list[T])) | ||
| ``` | ||
|
|
||
| A declared upper bound on `T` can make this relation true: | ||
|
|
||
| ```pyi | ||
| def bounded[T: int](): | ||
| static_assert(is_subtype_of(list[T], Top[list[int & Any]])) | ||
| ``` | ||
|
|
||
| ## Assignability | ||
|
|
||
| ### General | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -509,13 +509,47 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { | |
| debug_assert!(std::ptr::eq(self.builder, builder)); | ||
| } | ||
|
|
||
| /// Returns whether this constraint set never holds. | ||
| /// Returns whether this constraint set never holds, without checking the type variables' | ||
| /// declared bounds or constraints. Use [`Self::has_no_valid_solutions`] to include those. | ||
| pub(crate) fn is_never_satisfied(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { | ||
| let mut storage = self.builder.storage.borrow_mut(); | ||
| self.node | ||
| .is_never_satisfied(db, env, &mut storage, self.source_order) | ||
| } | ||
|
|
||
| /// Returns whether no specialization satisfying the type variables' upper bounds and | ||
| /// constraints can satisfy this constraint set. | ||
| /// | ||
| /// Unlike [`Self::is_never_satisfied`], this validates solutions against the type variables' | ||
| /// upper bounds and constraints. For example, `T = int` is not contradictory by itself, but has | ||
| /// no valid solution if `T` has an upper bound of `str`. | ||
| /// | ||
| /// If the solver reaches its computation limit, we do not know whether a valid solution exists. | ||
| /// This returns `false` in that case: stopping the search is not proof that there is no solution. | ||
| pub(crate) fn has_no_valid_solutions( | ||
| self, | ||
| db: &'db dyn Db, | ||
| env: &ProgramEnvironment<'db>, | ||
| ) -> bool { | ||
| if self.is_never_satisfied(db, env) { | ||
| return true; | ||
| } | ||
|
|
||
| let inferable = { | ||
| let storage = self.builder.storage.borrow(); | ||
| let Some(support) = storage.node_support(self.node) else { | ||
| return false; | ||
| }; | ||
| // For overlap, every mentioned type variable can choose a valid specialization. | ||
| TypeVarSet::from_typevars(db, support.iter().map(|id| storage.typevar_data(id))) | ||
| }; | ||
|
|
||
| matches!( | ||
| self.solutions(db, env, inferable), | ||
| Ok(Solutions::Unsatisfiable) | ||
| ) | ||
| } | ||
|
Comment on lines
+520
to
+551
Contributor
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. Maybe if this TODO get's resolved, we won't need this function? |
||
|
|
||
| /// Returns whether this constraint set is the `never` terminal. | ||
| /// | ||
| /// A nonterminal constraint set can also never be satisfied, so `false` does not prove that | ||
|
|
||
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.
Without
@final, we would now revealBox[T@box_with_default] | (T@box_with_default & Top[Box[Unknown]])in the firstisinstancebranch below. I believe this is correct (and was wrong onmain): it accounts for the possibility of a common subclass ofstrandBox(possibly with another specialization). Thereturn valueconsequently lead to an error.I added
@finalto restore the original intention of this test (similar inmatch.md).