diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md index a5154b450a64b..9c91adb52e2eb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md @@ -89,7 +89,7 @@ def _( reveal_type(k) # revealed: Unknown reveal_type(p) # revealed: Unknown reveal_type(q) # revealed: int | Unknown - reveal_type(r) # revealed: @Todo(generics) + reveal_type(r) # revealed: @Todo(unknown type subscript) ``` ## Invalid Collection based AST nodes diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md index 138478dbdf677..865bf073f1fdd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md @@ -137,7 +137,7 @@ from other import Literal a1: Literal[26] def f(): - reveal_type(a1) # revealed: @Todo(generics) + reveal_type(a1) # revealed: @Todo(unknown type subscript) ``` ## Detecting typing_extensions.Literal diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md index 171ade213e99b..52c51ea490c75 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -61,7 +61,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] reveal_type(e) # revealed: @Todo(full tuple[...] support) reveal_type(f) # revealed: @Todo(full tuple[...] support) reveal_type(g) # revealed: @Todo(full tuple[...] support) -reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)] +reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)] reveal_type(i) # revealed: tuple[str | int, str | int] reveal_type(j) # revealed: tuple[str | int] diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 3f6c5321c0349..57f385fc917ac 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1665,7 +1665,7 @@ functions are instances of that class: def f(): ... reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None -reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None +reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None ``` Some attributes are special-cased, however: @@ -1716,7 +1716,8 @@ reveal_type(False.real) # revealed: Literal[0] All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: ```py -reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(generics), /) -> bytes +# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes +reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool reveal_type(b"foo".endswith) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index c7a63beff2e38..9ca10a05c7a0a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd methods, even though it is not available on `types.MethodType`: ```py -reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None +reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None ``` ## Basic method calls on class objects and instances diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/red_knot_python_semantic/resources/mdtest/decorators.md index 3ba75ec10b0d0..b1233f3d0f210 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/decorators.md +++ b/crates/red_knot_python_semantic/resources/mdtest/decorators.md @@ -145,10 +145,10 @@ def f(x: int) -> int: return x**2 # TODO: Should be `_lru_cache_wrapper[int]` -reveal_type(f) # revealed: @Todo(generics) +reveal_type(f) # revealed: @Todo(specialized non-generic class) # TODO: Should be `int` -reveal_type(f(1)) # revealed: @Todo(generics) +reveal_type(f(1)) # revealed: @Todo(specialized non-generic class) ``` ## Lambdas as decorators diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md index 96a01d90d9e5f..8ff82e29dfef4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md @@ -61,7 +61,7 @@ from knot_extensions import Unknown def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) - reveal_type(a) # revealed: @Todo(generics) + reveal_type(a) # revealed: @Todo(specialized non-generic class) b = cast(Any, y) reveal_type(b) # revealed: Any diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index 9421d16c3622b..737e3455e0ed7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -164,12 +164,12 @@ consistent with each other. ```py class C[T]: - def __new__(cls, x: T) -> "C"[T]: + def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -181,7 +181,7 @@ class C[T]: reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -189,14 +189,14 @@ wrong_innards: C[int] = C("five") ```py class C[T]: - def __new__(cls, x: T) -> "C"[T]: + def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) def __init__(self, x: T) -> None: ... reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -204,25 +204,25 @@ wrong_innards: C[int] = C("five") ```py class C[T]: - def __new__(cls, *args, **kwargs) -> "C"[T]: + def __new__(cls, *args, **kwargs) -> "C[T]": return object.__new__(cls) def __init__(self, x: T) -> None: ... reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") class D[T]: - def __new__(cls, x: T) -> "D"[T]: + def __new__(cls, x: T) -> "D[T]": return object.__new__(cls) def __init__(self, *args, **kwargs) -> None: ... reveal_type(D(1)) # revealed: D[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`" wrong_innards: D[int] = D("five") ``` @@ -247,7 +247,7 @@ reveal_type(C(1, "string")) # revealed: C[Unknown] # error: [invalid-argument-type] reveal_type(C(1, True)) # revealed: C[Unknown] -# TODO: error for the correct reason +# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`" wrong_innards: C[int] = C("five", 1) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 7870e8b62f0ff..67e69940f3129 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -64,19 +64,19 @@ is.) from knot_extensions import is_fully_static, static_assert from typing import Any -def unbounded_unconstrained[T](t: list[T]) -> None: +def unbounded_unconstrained[T](t: T) -> None: static_assert(is_fully_static(T)) -def bounded[T: int](t: list[T]) -> None: +def bounded[T: int](t: T) -> None: static_assert(is_fully_static(T)) -def bounded_by_gradual[T: Any](t: list[T]) -> None: +def bounded_by_gradual[T: Any](t: T) -> None: static_assert(not is_fully_static(T)) -def constrained[T: (int, str)](t: list[T]) -> None: +def constrained[T: (int, str)](t: T) -> None: static_assert(is_fully_static(T)) -def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: +def constrained_by_gradual[T: (int, Any)](t: T) -> None: static_assert(not is_fully_static(T)) ``` @@ -99,7 +99,7 @@ class Base(Super): ... class Sub(Base): ... class Unrelated: ... -def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: +def unbounded_unconstrained[T, U](t: T, u: U) -> None: static_assert(is_assignable_to(T, T)) static_assert(is_assignable_to(T, object)) static_assert(not is_assignable_to(T, Super)) @@ -129,7 +129,7 @@ is a final class, since the typevar can still be specialized to `Never`.) from typing import Any from typing_extensions import final -def bounded[T: Super](t: list[T]) -> None: +def bounded[T: Super](t: T) -> None: static_assert(is_assignable_to(T, Super)) static_assert(not is_assignable_to(T, Sub)) static_assert(not is_assignable_to(Super, T)) @@ -140,7 +140,7 @@ def bounded[T: Super](t: list[T]) -> None: static_assert(not is_subtype_of(Super, T)) static_assert(not is_subtype_of(Sub, T)) -def bounded_by_gradual[T: Any](t: list[T]) -> None: +def bounded_by_gradual[T: Any](t: T) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) static_assert(is_assignable_to(T, Super)) @@ -158,7 +158,7 @@ def bounded_by_gradual[T: Any](t: list[T]) -> None: @final class FinalClass: ... -def bounded_final[T: FinalClass](t: list[T]) -> None: +def bounded_final[T: FinalClass](t: T) -> None: static_assert(is_assignable_to(T, FinalClass)) static_assert(not is_assignable_to(FinalClass, T)) @@ -172,14 +172,14 @@ true even if both typevars are bounded by the same final class, since you can sp typevars to `Never` in addition to that final class. ```py -def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None: +def two_bounded[T: Super, U: Super](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) -def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None: +def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -194,7 +194,7 @@ intersection of all of its constraints is a subtype of the typevar. ```py from knot_extensions import Intersection -def constrained[T: (Base, Unrelated)](t: list[T]) -> None: +def constrained[T: (Base, Unrelated)](t: T) -> None: static_assert(not is_assignable_to(T, Super)) static_assert(not is_assignable_to(T, Base)) static_assert(not is_assignable_to(T, Sub)) @@ -219,7 +219,7 @@ def constrained[T: (Base, Unrelated)](t: list[T]) -> None: static_assert(not is_subtype_of(Super | Unrelated, T)) static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) -def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None: +def constrained_by_gradual[T: (Base, Any)](t: T) -> None: static_assert(is_assignable_to(T, Super)) static_assert(is_assignable_to(T, Base)) static_assert(not is_assignable_to(T, Sub)) @@ -261,7 +261,7 @@ distinct constraints, meaning that there is (still) no guarantee that they will the same type. ```py -def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None: +def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -271,7 +271,7 @@ def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> Non @final class AnotherFinalClass: ... -def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None: +def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -290,7 +290,7 @@ non-singleton type. ```py from knot_extensions import is_singleton, is_single_valued, static_assert -def unbounded_unconstrained[T](t: list[T]) -> None: +def unbounded_unconstrained[T](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) ``` @@ -299,7 +299,7 @@ A bounded typevar is not a singleton, even if its bound is a singleton, since it specialized to `Never`. ```py -def bounded[T: None](t: list[T]) -> None: +def bounded[T: None](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) ``` @@ -310,14 +310,14 @@ specialize a constrained typevar to a subtype of a constraint.) ```py from typing_extensions import Literal -def constrained_non_singletons[T: (int, str)](t: list[T]) -> None: +def constrained_non_singletons[T: (int, str)](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) -def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None: +def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None: static_assert(is_singleton(T)) -def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None: +def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: static_assert(is_single_valued(T)) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index da09b7c7bc8f6..0d99019894009 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -174,13 +174,13 @@ S = TypeVar("S") def f(x: T) -> None: x: list[T] = [] - # TODO: error + # TODO: invalid-assignment error y: list[S] = [] # TODO: no error # error: [invalid-base] class C(Generic[T]): - # TODO: error + # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] # This is not an error, as shown in the previous test @@ -200,11 +200,11 @@ S = TypeVar("S") def f[T](x: T) -> None: x: list[T] = [] - # TODO: error + # TODO: invalid assignment error y: list[S] = [] class C[T]: - # TODO: error + # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] def m1(self, x: S) -> S: @@ -288,7 +288,7 @@ class C[T]: ok1: list[T] = [] class Bad: - # TODO: error + # TODO: error: cannot refer to T in nested scope bad: list[T] = [] class Inner[S]: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md b/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md new file mode 100644 index 0000000000000..1541a46fbca73 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md @@ -0,0 +1,277 @@ +# Variance + +```toml +[environment] +python-version = "3.12" +``` + +Type variables have a property called _variance_ that affects the subtyping and assignability +relations. Much more detail can be found in the [spec]. To summarize, each typevar is either +**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not +currently mentioned in the typing spec, but is a fourth case that we must consider.) + +For all of the examples below, we will consider a typevar `T`, a generic class using that typevar +`C[T]`, and two types `A` and `B`. + +## Covariance + +With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`. + +Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of +`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would +get from the sequence is a valid `int`. + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def receive(self) -> T: + raise ValueError + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +static_assert(not is_gradual_equivalent_to(C[B], C[A])) +static_assert(not is_gradual_equivalent_to(C[A], C[B])) +static_assert(not is_gradual_equivalent_to(C[A], C[Any])) +static_assert(not is_gradual_equivalent_to(C[B], C[Any])) +static_assert(not is_gradual_equivalent_to(C[Any], C[A])) +static_assert(not is_gradual_equivalent_to(C[Any], C[B])) +``` + +## Contravariance + +With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`. + +Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives +`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool` +that you pass into the consumer is a valid `int`. + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def send(self, value: T): ... + +static_assert(not is_assignable_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +static_assert(not is_gradual_equivalent_to(C[B], C[A])) +static_assert(not is_gradual_equivalent_to(C[A], C[B])) +static_assert(not is_gradual_equivalent_to(C[A], C[Any])) +static_assert(not is_gradual_equivalent_to(C[B], C[Any])) +static_assert(not is_gradual_equivalent_to(C[Any], C[A])) +static_assert(not is_gradual_equivalent_to(C[Any], C[B])) +``` + +## Invariance + +With an invariant typevar, _no_ specializations of the generic class are subtypes of each other. + +This often occurs for types that are both producers _and_ consumers, like a mutable `list`. +Iterating over the elements in a list would work with a covariant typevar, just like with the +"producer" type above. Appending elements to a list would work with a contravariant typevar, just +like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant +at the same time! + +If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list +of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list +would no longer only contain elements that are subtypes of `bool`. + +Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a +mutable list of `int`s, since you might try to extract elements from the list: you expect every +element that you extract to be a subtype of `bool`, but the list can contain any `int`. + +In the end, if you expect a mutable list, you must always be given a list of exactly that type, +since we can't know in advance which of the allowed methods you'll want to use. + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def send(self, value: T): ... + def receive(self) -> T: + raise ValueError + +static_assert(not is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +static_assert(not is_gradual_equivalent_to(C[B], C[A])) +static_assert(not is_gradual_equivalent_to(C[A], C[B])) +static_assert(not is_gradual_equivalent_to(C[A], C[Any])) +static_assert(not is_gradual_equivalent_to(C[B], C[Any])) +static_assert(not is_gradual_equivalent_to(C[Any], C[A])) +static_assert(not is_gradual_equivalent_to(C[Any], C[B])) +``` + +## Bivariance + +With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact, +equivalent to) each other. + +This is a bit of pathological case, which really only happens when the class doesn't use the typevar +at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_ +the typevar was used.) + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + pass + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[A], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[A], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[B], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[Any], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[Any], C[B])) +``` + +[spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index f862099751299..2ddb0f404da89 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -344,7 +344,7 @@ class Foo(Protocol): # `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]` # # `frozenset[Literal["x", "y", "z", "method_member"]]` -reveal_type(get_protocol_members(Foo)) # revealed: @Todo(generics) +reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class) ``` Calling `get_protocol_members` on a non-protocol class raises an error at runtime: @@ -353,7 +353,7 @@ Calling `get_protocol_members` on a non-protocol class raises an error at runtim class NotAProtocol: ... # TODO: should emit `[invalid-protocol]` error, should reveal `Unknown` -reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(generics) +reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class) ``` Certain special attributes and methods are not considered protocol members at runtime, and should @@ -372,7 +372,7 @@ class Lumberjack(Protocol): self.x = x # TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]` -reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(generics) +reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class) ``` ## Subtyping of protocols with attribute members diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 65d9a930aff1e..8cbe44952647b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -14,7 +14,7 @@ reveal_type(__package__) # revealed: str | None reveal_type(__doc__) # revealed: str | None reveal_type(__spec__) # revealed: ModuleSpec | None -reveal_type(__path__) # revealed: @Todo(generics) +reveal_type(__path__) # revealed: @Todo(specialized non-generic class) class X: reveal_type(__name__) # revealed: str @@ -59,7 +59,7 @@ reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: ob reveal_type(typing.__class__) # revealed: Literal[ModuleType] # TODO: needs support generics; should be `dict[str, Any]`: -reveal_type(typing.__dict__) # revealed: @Todo(generics) +reveal_type(typing.__dict__) # revealed: @Todo(specialized non-generic class) ``` Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with @@ -92,8 +92,8 @@ import foo from foo import __dict__ as foo_dict # TODO: needs support generics; should be `dict[str, Any]` for both of these: -reveal_type(foo.__dict__) # revealed: @Todo(generics) -reveal_type(foo_dict) # revealed: @Todo(generics) +reveal_type(foo.__dict__) # revealed: @Todo(specialized non-generic class) +reveal_type(foo_dict) # revealed: @Todo(specialized non-generic class) ``` ## Conditionally global or `ModuleType` attribute diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 5f9264f6faa16..9bb53d4e67225 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -15,7 +15,7 @@ reveal_type(x) # revealed: list reveal_type(x[0]) # revealed: Unknown | @Todo(Support for `typing.TypeVar` instances in type expressions) # TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo(generics) +reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) # error: [call-non-callable] reveal_type(x["a"]) # revealed: Unknown diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md index 45bbf07fac20c..398847ecbe0e0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/union_types.md @@ -169,6 +169,11 @@ def _( ## Unions of literals with `AlwaysTruthy` and `AlwaysFalsy` +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import Literal from knot_extensions import AlwaysTruthy, AlwaysFalsy diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 6782de5f69dbd..68fbc1d2fd274 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1362,6 +1362,10 @@ impl<'db> Type<'db> { ) } + (Type::Instance(self_instance), Type::Instance(target_instance)) => { + self_instance.is_assignable_to(db, target_instance) + } + (Type::Callable(self_callable), Type::Callable(target_callable)) => { self_callable.is_assignable_to(db, target_callable) } @@ -1376,7 +1380,7 @@ impl<'db> Type<'db> { .into_callable_type(db) .is_assignable_to(db, target), - // TODO other types containing gradual forms (e.g. generics containing Any/Unknown) + // TODO other types containing gradual forms _ => self.is_subtype_of(db, target), } } @@ -1396,6 +1400,7 @@ impl<'db> Type<'db> { } (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right), + (Type::Instance(left), Type::Instance(right)) => left.is_equivalent_to(db, right), _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), } } @@ -1448,6 +1453,10 @@ impl<'db> Type<'db> { (Type::TypeVar(first), Type::TypeVar(second)) => first == second, + (Type::Instance(first), Type::Instance(second)) => { + first.is_gradual_equivalent_to(db, second) + } + (Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second), (Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second), diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index f22e8a668eac2..e8e53a2e3f561 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -257,11 +257,107 @@ impl<'db> ClassType<'db> { class_literal.is_final(db) } + /// If `self` and `other` are generic aliases of the same generic class, returns their + /// corresponding specializations. + fn compatible_specializations( + self, + db: &'db dyn Db, + other: ClassType<'db>, + ) -> Option<(Specialization<'db>, Specialization<'db>)> { + match (self, other) { + (ClassType::Generic(self_generic), ClassType::Generic(other_generic)) => { + if self_generic.origin(db) == other_generic.origin(db) { + Some(( + self_generic.specialization(db), + other_generic.specialization(db), + )) + } else { + None + } + } + _ => None, + } + } + /// Return `true` if `other` is present in this class's MRO. pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { // `is_subclass_of` is checking the subtype relation, in which gradual types do not // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db).contains(&ClassBase::Class(other)) + if self.iter_mro(db).contains(&ClassBase::Class(other)) { + return true; + } + + // `self` is a subclass of `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_subtype_of(db, other_specialization) { + return true; + } + } + + false + } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + if self == other { + return true; + } + + // `self` is equivalent to `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_equivalent_to(db, other_specialization) { + return true; + } + } + + false + } + + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. + if self.is_subclass_of(db, other) { + return true; + } + + // `self` is assignable to `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_assignable_to(db, other_specialization) { + return true; + } + } + + false + } + + pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + if self == other { + return true; + } + + // `self` is equivalent to `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_gradual_equivalent_to(db, other_specialization) { + return true; + } + } + + false } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. @@ -1516,6 +1612,22 @@ impl<'db> InstanceType<'db> { // N.B. The subclass relation is fully static self.class.is_subclass_of(db, other.class) } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { + self.class.is_equivalent_to(db, other.class) + } + + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { + self.class.is_assignable_to(db, other.class) + } + + pub(super) fn is_gradual_equivalent_to( + self, + db: &'db dyn Db, + other: InstanceType<'db>, + ) -> bool { + self.class.is_gradual_equivalent_to(db, other.class) + } } impl<'db> From> for Type<'db> { diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 3e350112f9abf..ef5d66a772eed 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -181,6 +181,118 @@ impl<'db> Specialization<'db> { .find(|(var, _)| **var == typevar) .map(|(_, ty)| *ty) } + + pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { + return false; + } + + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type <: other_type + // - contravariant: verify that other_type <: self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make subtyping false + if !self_type.is_equivalent_to(db, *other_type) { + return false; + } + } + + true + } + + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { + return false; + } + + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type == other_type + // - contravariant: verify that other_type == self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make equivalence false + if !self_type.is_equivalent_to(db, *other_type) { + return false; + } + } + + true + } + + pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { + continue; + } + + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type <: other_type + // - contravariant: verify that other_type <: self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make assignability false + if !self_type.is_gradual_equivalent_to(db, *other_type) { + return false; + } + } + + true + } + + pub(crate) fn is_gradual_equivalent_to( + self, + db: &'db dyn Db, + other: Specialization<'db>, + ) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type == other_type + // - contravariant: verify that other_type == self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make equivalence false + if !self_type.is_gradual_equivalent_to(db, *other_type) { + return false; + } + } + + true + } } /// Performs type inference between parameter annotations and argument types, producing a diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index a81c97c9ce2f8..4107b39775324 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -6044,12 +6044,7 @@ impl<'db> TypeInferenceBuilder<'db> { // special cases, too. let value_ty = self.infer_expression(value); if let Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) = value_ty { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - generic_class, - slice, - ); + return self.infer_explicit_class_specialization(subscript, value_ty, generic_class); } let slice_ty = self.infer_expression(slice); @@ -6061,8 +6056,8 @@ impl<'db> TypeInferenceBuilder<'db> { subscript: &ast::ExprSubscript, value_ty: Type<'db>, generic_class: GenericClass<'db>, - slice_node: &ast::Expr, ) -> Type<'db> { + let slice_node = subscript.slice.as_ref(); let mut call_argument_types = match slice_node { ast::Expr::Tuple(tuple) => CallArgumentTypes::positional( tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), @@ -7183,9 +7178,24 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(slice); value_ty } + Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) => { + let specialized_class = + self.infer_explicit_class_specialization(subscript, value_ty, generic_class); + specialized_class + .in_type_expression(self.db()) + .unwrap_or(Type::unknown()) + } + Type::ClassLiteral(ClassLiteralType::NonGeneric(_)) => { + // TODO: Once we know that e.g. `list` is generic, emit a diagnostic if you try to + // specialize a non-generic class. + self.infer_type_expression(slice); + todo_type!("specialized non-generic class") + } _ => { + // TODO: Emit a diagnostic once we've implemented all valid subscript type + // expressions. self.infer_type_expression(slice); - todo_type!("generics") + todo_type!("unknown type subscript") } } }