From 5a570c8e6d227b1f4270987b124e853ccd7b2af3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 13 Aug 2025 15:51:59 -0700 Subject: [PATCH] [ty] fix deferred name loading in PEP695 generic classes/functions (#19888) ## Summary For PEP 695 generic functions and classes, there is an extra "type params scope" (a child of the outer scope, and wrapping the body scope) in which the type parameters are defined; class bases and function parameter/return annotations are resolved in that type-params scope. This PR fixes some longstanding bugs in how we resolve name loads from inside these PEP 695 type parameter scopes, and also defers type inference of PEP 695 typevar bounds/constraints/default, so we can handle cycles without panicking. We were previously treating these type-param scopes as lazy nested scopes, which is wrong. In fact they are eager nested scopes; the class `C` here inherits `int`, not `str`, and previously we got that wrong: ```py Base = int class C[T](Base): ... Base = str ``` But certain syntactic positions within type param scopes (typevar bounds/constraints/defaults) are lazy at runtime, and we should use deferred name resolution for them. This also means they can have cycles; in order to handle that without panicking in type inference, we need to actually defer their type inference until after we have constructed the `TypeVarInstance`. PEP 695 does specify that typevar bounds and constraints cannot be generic, and that typevar defaults can only reference prior typevars, not later ones. This reduces the scope of (valid from the type-system perspective) cycles somewhat, although cycles are still possible (e.g. `class C[T: list[C]]`). And this is a type-system-only restriction; from the runtime perspective an "invalid" case like `class C[T: T]` actually works fine. I debated whether to implement the PEP 695 restrictions as a way to avoid some cycles up-front, but I ended up deciding against that; I'd rather model the runtime name-resolution semantics accurately, and implement the PEP 695 restrictions as a separate diagnostic on top. (This PR doesn't yet implement those diagnostics, thus some `# TODO: error` in the added tests.) Introducing the possibility of cyclic typevars made typevar display potentially stack overflow. For now I've handled this by simply removing typevar details (bounds/constraints/default) from typevar display. This impacts display of two kinds of types. If you `reveal_type(T)` on an unbound `T` you now get just `typing.TypeVar` instead of `typing.TypeVar("T", ...)` where `...` is the bound/constraints/default. This matches pyright and mypy; pyrefly uses `type[TypeVar[T]]` which seems a bit confusing, but does include the name. (We could easily include the name without cycle issues, if there's a syntax we like for that.) It also means that displaying a generic function type like `def f[T: int](x: T) -> T: ...` now displays as `f[T](x: T) -> T` instead of `f[T: int](x: T) -> T`. This matches pyright and pyrefly; mypy does include bound/constraints/defaults of typevars in function/callable type display. If we wanted to add this, we would either need to thread a visitor through all the type display code, or add a `decycle` type transformation that replaced recursive reoccurrence of a type with a marker. ## Test Plan Added mdtests and modified existing tests to improve their correctness. After this PR, there's only a single remaining py-fuzzer seed in the 0-500 range that panics! (Before this PR, there were 10; the fuzzer likes to generate cyclic PEP 695 syntax.) ## Ecosystem report It's all just the changes to `TypeVar` display. --- crates/ty_ide/src/hover.rs | 8 +- crates/ty_ide/src/inlay_hints.rs | 6 +- .../corpus/cyclic_pep695_typevars.py | 5 + .../except_handler_with_Any_bound_typevar.py | 6 - .../mdtest/generics/legacy/variables.md | 8 +- .../mdtest/generics/pep695/classes.md | 20 ++- .../mdtest/generics/pep695/variables.md | 94 ++++++++++- .../resources/mdtest/protocols.md | 1 + .../resources/mdtest/scopes/eager.md | 43 +++++ ..._Cover_non-keyword_re…_(707b284610419a54).snap | 18 +-- .../ty_python_semantic/src/semantic_index.rs | 10 +- .../src/semantic_index/builder.rs | 8 +- .../src/semantic_index/definition.rs | 7 + .../src/semantic_index/scope.rs | 26 +-- crates/ty_python_semantic/src/types.rs | 151 +++++++++++++++--- .../ty_python_semantic/src/types/call/bind.rs | 2 +- crates/ty_python_semantic/src/types/class.rs | 8 +- .../ty_python_semantic/src/types/display.rs | 84 +--------- .../ty_python_semantic/src/types/generics.rs | 6 +- crates/ty_python_semantic/src/types/infer.rs | 142 ++++++++-------- .../src/types/signatures.rs | 6 +- .../src/types/subclass_of.rs | 9 +- 22 files changed, 429 insertions(+), 239 deletions(-) create mode 100644 crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 5bb1d4239f..0918d86a4d 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -728,11 +728,11 @@ mod tests { ); // TODO: This should render T@Alias once we create GenericContexts for type alias scopes. - assert_snapshot!(test.hover(), @r#" - typing.TypeVar("T", bound=int, default=bool) + assert_snapshot!(test.hover(), @r###" + typing.TypeVar --------------------------------------------- ```python - typing.TypeVar("T", bound=int, default=bool) + typing.TypeVar ``` --------------------------------------------- info[hover]: Hovered content is @@ -743,7 +743,7 @@ mod tests { | | | source | - "#); + "###); } #[test] diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 2ae416e9b8..b38e50a011 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -793,17 +793,17 @@ mod tests { identity('hello')", ); - assert_snapshot!(test.inlay_hints(), @r#" + assert_snapshot!(test.inlay_hints(), @r###" from typing import TypeVar, Generic - T[: typing.TypeVar("T")] = TypeVar([name=]'T') + T[: typing.TypeVar] = TypeVar([name=]'T') def identity(x: T) -> T: return x identity([x=]42) identity([x=]'hello') - "#); + "###); } #[test] diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py new file mode 100644 index 0000000000..5c9f592768 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py @@ -0,0 +1,5 @@ +def name_1[name_0: name_0](name_2: name_0): + try: + pass + except name_2: + pass diff --git a/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py b/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py index a951d5034f..d6a3732a3c 100644 --- a/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py +++ b/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py @@ -1,9 +1,3 @@ -def name_1[name_0: name_0](name_2: name_0): - try: - pass - except name_2: - pass - from typing import Any def name_2[T: Any](x: T): diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 1912f48d2e..d34c642bb3 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -20,7 +20,7 @@ from typing import TypeVar T = TypeVar("T") reveal_type(type(T)) # revealed: -reveal_type(T) # revealed: typing.TypeVar("T") +reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__name__) # revealed: Literal["T"] ``` @@ -80,7 +80,7 @@ from typing import TypeVar T = TypeVar("T", default=int) reveal_type(type(T)) # revealed: -reveal_type(T) # revealed: typing.TypeVar("T", default=int) +reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__default__) # revealed: int reveal_type(T.__bound__) # revealed: None reveal_type(T.__constraints__) # revealed: tuple[()] @@ -116,7 +116,7 @@ from typing import TypeVar T = TypeVar("T", bound=int) reveal_type(type(T)) # revealed: -reveal_type(T) # revealed: typing.TypeVar("T", bound=int) +reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__bound__) # revealed: int reveal_type(T.__constraints__) # revealed: tuple[()] @@ -131,7 +131,7 @@ from typing import TypeVar T = TypeVar("T", int, str) reveal_type(type(T)) # revealed: -reveal_type(T) # revealed: typing.TypeVar("T", int, str) +reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__constraints__) # revealed: tuple[int, str] S = TypeVar("S") diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 9b82e4ff9a..261c3d370d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -391,6 +391,7 @@ wrong_innards: C[int] = C("five", 1) ### Some `__init__` overloads only apply to certain specializations ```py +from __future__ import annotations from typing import overload class C[T]: @@ -541,6 +542,23 @@ class WithOverloadedMethod[T]: reveal_type(WithOverloadedMethod[int].method) ``` +## Scoping of typevars + +### No back-references + +Typevar bounds/constraints/defaults are lazy, but cannot refer to later typevars: + +```py +# TODO error +class C[S: T, T]: + pass + +class D[S: X]: + pass + +X = int +``` + ## Cyclic class definitions ### F-bounded quantification @@ -591,7 +609,7 @@ class Derived[T](list[Derived[T]]): ... Inheritance that would result in a cyclic MRO is detected as an error. -```py +```pyi # error: [cyclic-class-definition] class C[T](C): ... diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index f0c8ce2d04..ebf69f7e4e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -2,7 +2,7 @@ ```toml [environment] -python-version = "3.12" +python-version = "3.13" ``` [PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables. @@ -17,7 +17,7 @@ instances of `typing.TypeVar`, just like legacy type variables. ```py def f[T](): reveal_type(type(T)) # revealed: - reveal_type(T) # revealed: typing.TypeVar("T") + reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__name__) # revealed: Literal["T"] ``` @@ -33,7 +33,7 @@ python-version = "3.13" ```py def f[T = int](): reveal_type(type(T)) # revealed: - reveal_type(T) # revealed: typing.TypeVar("T", default=int) + reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__default__) # revealed: int reveal_type(T.__bound__) # revealed: None reveal_type(T.__constraints__) # revealed: tuple[()] @@ -66,7 +66,7 @@ class Invalid[S = T]: ... ```py def f[T: int](): reveal_type(type(T)) # revealed: - reveal_type(T) # revealed: typing.TypeVar("T", bound=int) + reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__bound__) # revealed: int reveal_type(T.__constraints__) # revealed: tuple[()] @@ -79,7 +79,7 @@ def g[S](): ```py def f[T: (int, str)](): reveal_type(type(T)) # revealed: - reveal_type(T) # revealed: typing.TypeVar("T", int, str) + reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__constraints__) # revealed: tuple[int, str] reveal_type(T.__bound__) # revealed: None @@ -745,4 +745,88 @@ def constrained[T: (int, str)](x: T): reveal_type(type(x)) # revealed: type[int] | type[str] ``` +## Cycles + +### Bounds and constraints + +A typevar's bounds and constraints cannot be generic, cyclic or otherwise: + +```py +from typing import Any + +# TODO: error +def f[S, T: list[S]](x: S, y: T) -> S | T: + return x or y + +# TODO: error +class C[S, T: list[S]]: + x: S + y: T + +reveal_type(C[int, list[Any]]().x) # revealed: int +reveal_type(C[int, list[Any]]().y) # revealed: list[Any] + +# TODO: error +def g[T: list[T]](x: T) -> T: + return x + +# TODO: error +class D[T: list[T]]: + x: T + +reveal_type(D[list[Any]]().x) # revealed: list[Any] + +# TODO: error +def h[S, T: (list[S], str)](x: S, y: T) -> S | T: + return x or y + +# TODO: error +class E[S, T: (list[S], str)]: + x: S + y: T + +reveal_type(E[int, str]().x) # revealed: int +reveal_type(E[int, str]().y) # revealed: str + +# TODO: error +def i[T: (list[T], str)](x: T) -> T: + return x + +# TODO: error +class F[T: (list[T], str)]: + x: T + +reveal_type(F[list[Any]]().x) # revealed: list[Any] +``` + +However, they are lazily evaluated and can cyclically refer to their own type: + +```py +class G[T: list[G]]: + x: T + +reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]] +``` + +### Defaults + +Defaults can be generic, but can only refer to earlier typevars: + +```py +class C[T, U = T]: + x: T + y: U + +reveal_type(C[int, str]().x) # revealed: int +reveal_type(C[int, str]().y) # revealed: str +reveal_type(C[int]().x) # revealed: int +reveal_type(C[int]().y) # revealed: int + +# TODO: error +class D[T = T]: + x: T + +reveal_type(D().x) # revealed: T@D +``` + [pep 695]: https://peps.python.org/pep-0695/ diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 3b4b18c2aa..e68cc538ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2051,6 +2051,7 @@ python-version = "3.12" ``` ```py +from __future__ import annotations from typing import cast, Protocol class Iterator[T](Protocol): diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/eager.md b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md index b8bc74d716..286f2eb261 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/eager.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md @@ -410,4 +410,47 @@ reveal_type(C.var) # revealed: int | str x = str ``` +### Annotation scopes + +```toml +[environment] +python-version = "3.12" +``` + +#### Type alias annotation scopes are lazy + +```py +type Foo = Bar + +class Bar: + pass + +def _(x: Foo): + if isinstance(x, Bar): + reveal_type(x) # revealed: Bar + else: + reveal_type(x) # revealed: Never +``` + +#### Type-param scopes are eager, but bounds/constraints are deferred + +```py +# error: [unresolved-reference] +class D[T](Bar): + pass + +class E[T: Bar]: + pass + +# error: [unresolved-reference] +def g[T](x: Bar): + pass + +def h[T: Bar](x: T): + pass + +class Bar: + pass +``` + [generators]: https://docs.python.org/3/reference/expressions.html#generator-expressions diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap index f3f4563dc5..52a55ed12c 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap @@ -86,7 +86,7 @@ error[call-non-callable]: Object of type `Literal[5]` is not callable | ^^^^ | info: Union variant `Literal[5]` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `call-non-callable` is enabled by default ``` @@ -101,7 +101,7 @@ error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable ( | ^^^^ | info: Union variant `PossiblyNotCallable` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `call-non-callable` is enabled by default ``` @@ -116,7 +116,7 @@ error[missing-argument]: No argument provided for required parameter `b` of func | ^^^^ | info: Union variant `def f3(a: int, b: int) -> int` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `missing-argument` is enabled by default ``` @@ -152,7 +152,7 @@ info: Overload implementation defined here 28 | return x + y if x and y else None | info: Union variant `Overload[() -> None, (x: str, y: str) -> str]` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `no-matching-overload` is enabled by default ``` @@ -176,7 +176,7 @@ info: Function defined here 8 | return 0 | info: Union variant `def f2(name: str) -> int` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `invalid-argument-type` is enabled by default ``` @@ -199,8 +199,8 @@ info: Type variable defined here | ^^^^^^ 14 | return 0 | -info: Union variant `def f4[T: str](x: T@f4) -> int` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Union variant `def f4[T](x: T@f4) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `invalid-argument-type` is enabled by default ``` @@ -227,7 +227,7 @@ info: Matching overload defined here info: Non-matching overloads for function `f5`: info: () -> None info: Union variant `Overload[() -> None, (x: str) -> str]` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `invalid-argument-type` is enabled by default ``` @@ -242,7 +242,7 @@ error[too-many-positional-arguments]: Too many positional arguments to function | ^ | info: Union variant `def f1() -> int` is incompatible with this call site -info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T: str](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `too-many-positional-arguments` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 2d429a4fec..c195059b38 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -167,7 +167,7 @@ pub(crate) fn attribute_scopes<'db, 's>( ChildrenIter::new(index, class_scope_id).filter_map(move |(child_scope_id, scope)| { let (function_scope_id, function_scope) = - if scope.node().scope_kind() == ScopeKind::Annotation { + if scope.node().scope_kind() == ScopeKind::TypeParams { // This could be a generic method with a type-params scope. // Go one level deeper to find the function scope. The first // descendant is the (potential) function scope. @@ -597,8 +597,8 @@ impl<'a> Iterator for VisibleAncestorsIter<'a> { // Skip class scopes for subsequent scopes (following Python's lexical scoping rules) // Exception: type parameter scopes can see names defined in an immediately-enclosing class scope if scope.kind() == ScopeKind::Class { - // Allow type parameter scopes to see their immediately-enclosing class scope exactly once - if self.starting_scope_kind.is_type_parameter() && self.yielded_count == 2 { + // Allow annotation scopes to see their immediately-enclosing class scope exactly once + if self.starting_scope_kind.is_annotation() && self.yielded_count == 2 { return Some((scope_id, scope)); } continue; @@ -1317,7 +1317,7 @@ def func[T](): panic!("expected one child scope"); }; - assert_eq!(ann_scope.kind(), ScopeKind::Annotation); + assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); assert_eq!( ann_scope_id.to_scope_id(&db, file).name(&db, &module), "func" @@ -1361,7 +1361,7 @@ class C[T]: panic!("expected one child scope"); }; - assert_eq!(ann_scope.kind(), ScopeKind::Annotation); + assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C"); let ann_table = index.place_table(ann_scope_id); assert_eq!(names(&ann_table), vec!["T"]); diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index e8a993fa10..1a9b6b01a9 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -199,7 +199,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { match self.scopes[parent.file_scope_id].kind() { ScopeKind::Class => Some(parent.file_scope_id), - ScopeKind::Annotation => { + ScopeKind::TypeParams => { // If the function is generic, the parent scope is an annotation scope. // In this case, we need to go up one level higher to find the class scope. let grandparent = scopes_rev.next()?; @@ -2637,7 +2637,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { ScopeKind::Comprehension | ScopeKind::Module | ScopeKind::TypeAlias - | ScopeKind::Annotation => {} + | ScopeKind::TypeParams => {} } } false @@ -2652,7 +2652,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { ScopeKind::Comprehension | ScopeKind::Module | ScopeKind::TypeAlias - | ScopeKind::Annotation => {} + | ScopeKind::TypeParams => {} } } false @@ -2664,7 +2664,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { match scope.kind() { ScopeKind::Class | ScopeKind::Comprehension => return false, ScopeKind::Function | ScopeKind::Lambda => return true, - ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::Annotation => {} + ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::TypeParams => {} } } false diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index edefca65e9..0e44e1a9c4 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -702,6 +702,13 @@ impl DefinitionKind<'_> { ) } + pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef> { + match self { + DefinitionKind::TypeVar(type_var) => Some(type_var), + _ => None, + } + } + /// Returns the [`TextRange`] of the definition target. /// /// A definition target would mainly be the node representing the place being defined i.e., diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index 6c3fcdbd71..b29e732305 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -29,8 +29,8 @@ impl<'db> ScopeId<'db> { self.node(db).scope_kind().is_function_like() } - pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { - self.node(db).scope_kind().is_type_parameter() + pub(crate) fn is_annotation(self, db: &'db dyn Db) -> bool { + self.node(db).scope_kind().is_annotation() } pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind { @@ -200,7 +200,7 @@ impl ScopeLaziness { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum ScopeKind { Module, - Annotation, + TypeParams, Class, Function, Lambda, @@ -215,18 +215,18 @@ impl ScopeKind { pub(crate) const fn laziness(self) -> ScopeLaziness { match self { - ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => ScopeLaziness::Eager, - ScopeKind::Annotation - | ScopeKind::Function - | ScopeKind::Lambda - | ScopeKind::TypeAlias => ScopeLaziness::Lazy, + ScopeKind::Module + | ScopeKind::Class + | ScopeKind::Comprehension + | ScopeKind::TypeParams => ScopeLaziness::Eager, + ScopeKind::Function | ScopeKind::Lambda | ScopeKind::TypeAlias => ScopeLaziness::Lazy, } } pub(crate) const fn visibility(self) -> ScopeVisibility { match self { ScopeKind::Module | ScopeKind::Class => ScopeVisibility::Public, - ScopeKind::Annotation + ScopeKind::TypeParams | ScopeKind::TypeAlias | ScopeKind::Function | ScopeKind::Lambda @@ -239,7 +239,7 @@ impl ScopeKind { // symbol table also uses the term "function-like" for these scopes. matches!( self, - ScopeKind::Annotation + ScopeKind::TypeParams | ScopeKind::Function | ScopeKind::Lambda | ScopeKind::TypeAlias @@ -255,8 +255,8 @@ impl ScopeKind { matches!(self, ScopeKind::Module) } - pub(crate) const fn is_type_parameter(self) -> bool { - matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) + pub(crate) const fn is_annotation(self) -> bool { + matches!(self, ScopeKind::TypeParams | ScopeKind::TypeAlias) } pub(crate) const fn is_non_lambda_function(self) -> bool { @@ -388,7 +388,7 @@ impl NodeWithScopeKind { Self::Lambda(_) => ScopeKind::Lambda, Self::FunctionTypeParameters(_) | Self::ClassTypeParameters(_) - | Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation, + | Self::TypeAliasTypeParameters(_) => ScopeKind::TypeParams, Self::TypeAlias(_) => ScopeKind::TypeAlias, Self::ListComprehension(_) | Self::SetComprehension(_) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 35f52a652a..d966d9008a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5291,7 +5291,7 @@ impl<'db> Type<'db> { db, Name::new(format!("{}'instance", typevar.name(db))), None, - Some(bound_or_constraints), + Some(bound_or_constraints.into()), typevar.variance(db), None, typevar.kind(db), @@ -5482,7 +5482,7 @@ impl<'db> Type<'db> { db, ast::name::Name::new_static("Self"), Some(class_definition), - Some(TypeVarBoundOrConstraints::UpperBound(instance)), + Some(TypeVarBoundOrConstraints::UpperBound(instance).into()), TypeVarVariance::Invariant, None, TypeVarKind::Implicit, @@ -6439,9 +6439,7 @@ impl<'db> KnownInstanceType<'db> { // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. - KnownInstanceType::TypeVar(typevar) => { - write!(f, "typing.TypeVar({})", typevar.display(self.db)) - } + KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"), KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"), KnownInstanceType::Field(field) => { f.write_str("dataclasses.Field[")?; @@ -6862,14 +6860,17 @@ pub struct TypeVarInstance<'db> { /// The type var's definition (None if synthesized) pub definition: Option>, - /// The upper bound or constraint on the type of this TypeVar - bound_or_constraints: Option>, + /// The upper bound or constraint on the type of this TypeVar, if any. Don't use this field + /// directly; use the `bound_or_constraints` (or `upper_bound` and `constraints`) methods + /// instead (to evaluate any lazy bound or constraints). + _bound_or_constraints: Option>, /// The variance of the TypeVar variance: TypeVarVariance, - /// The default type for this TypeVar - default_ty: Option>, + /// The default type for this TypeVar, if any. Don't use this field directly, use the + /// `default_type` method instead (to evaluate any lazy default). + _default: Option>, pub kind: TypeVarKind, } @@ -6885,11 +6886,12 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( if let Some(bounds) = typevar.bound_or_constraints(db) { walk_type_var_bounds(db, bounds, visitor); } - if let Some(default_type) = typevar.default_ty(db) { + if let Some(default_type) = typevar.default_type(db) { visitor.visit_type(db, default_type); } } +#[salsa::tracked] impl<'db> TypeVarInstance<'db> { pub(crate) fn with_binding_context( self, @@ -6919,15 +6921,50 @@ impl<'db> TypeVarInstance<'db> { } } + pub(crate) fn bound_or_constraints( + self, + db: &'db dyn Db, + ) -> Option> { + self._bound_or_constraints(db).and_then(|w| match w { + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { + Some(bound_or_constraints) + } + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self.lazy_bound(db), + TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self.lazy_constraints(db), + }) + } + + pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + self._default(db).and_then(|d| match d { + TypeVarDefaultEvaluation::Eager(ty) => Some(ty), + TypeVarDefaultEvaluation::Lazy => self.lazy_default(db), + }) + } + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { Self::new( db, self.name(db), self.definition(db), - self.bound_or_constraints(db) - .map(|b| b.normalized_impl(db, visitor)), + self._bound_or_constraints(db) + .and_then(|bound_or_constraints| match bound_or_constraints { + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { + Some(bound_or_constraints.normalized_impl(db, visitor).into()) + } + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self + .lazy_bound(db) + .map(|bound| bound.normalized_impl(db, visitor).into()), + TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self + .lazy_constraints(db) + .map(|constraints| constraints.normalized_impl(db, visitor).into()), + }), self.variance(db), - self.default_ty(db).map(|d| d.normalized_impl(db, visitor)), + self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => Some(ty.normalized_impl(db, visitor).into()), + TypeVarDefaultEvaluation::Lazy => self + .lazy_default(db) + .map(|ty| ty.normalized_impl(db, visitor).into()), + }), self.kind(db), ) } @@ -6937,13 +6974,59 @@ impl<'db> TypeVarInstance<'db> { db, self.name(db), self.definition(db), - self.bound_or_constraints(db) - .map(|b| b.materialize(db, variance)), + self._bound_or_constraints(db) + .and_then(|bound_or_constraints| match bound_or_constraints { + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { + Some(bound_or_constraints.materialize(db, variance).into()) + } + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self + .lazy_bound(db) + .map(|bound| bound.materialize(db, variance).into()), + TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self + .lazy_constraints(db) + .map(|constraints| constraints.materialize(db, variance).into()), + }), self.variance(db), - self.default_ty(db), + self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => Some(ty.materialize(db, variance).into()), + TypeVarDefaultEvaluation::Lazy => self + .lazy_default(db) + .map(|ty| ty.materialize(db, variance).into()), + }), self.kind(db), ) } + + #[salsa::tracked] + fn lazy_bound(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db)?; + let module = parsed_module(db, definition.file(db)).load(db); + let typevar_node = definition.kind(db).as_typevar()?.node(&module); + let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?); + Some(TypeVarBoundOrConstraints::UpperBound(ty)) + } + + #[salsa::tracked] + fn lazy_constraints(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db)?; + let module = parsed_module(db, definition.file(db)).load(db); + let typevar_node = definition.kind(db).as_typevar()?.node(&module); + let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?) + .into_union()?; + Some(TypeVarBoundOrConstraints::Constraints(ty)) + } + + #[salsa::tracked] + fn lazy_default(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db)?; + let module = parsed_module(db, definition.file(db)).load(db); + let typevar_node = definition.kind(db).as_typevar()?.node(&module); + Some(definition_expression_type( + db, + definition, + typevar_node.default.as_ref()?, + )) + } } /// Where a type variable is bound and usable. @@ -7008,10 +7091,10 @@ impl<'db> BoundTypeVarInstance<'db> { /// By using `U` in the generic class, it becomes bound, and so we have a /// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value /// (resulting in `T@C`). - pub(crate) fn default_ty(self, db: &'db dyn Db) -> Option> { + pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { let binding_context = self.binding_context(db); self.typevar(db) - .default_ty(db) + .default_type(db) .map(|ty| ty.apply_type_mapping(db, &TypeMapping::BindLegacyTypevars(binding_context))) } @@ -7054,6 +7137,38 @@ impl TypeVarVariance { } } +/// Whether a typevar default is eagerly specified or lazily evaluated. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum TypeVarDefaultEvaluation<'db> { + /// The default type is lazily evaluated. + Lazy, + /// The default type is eagerly specified. + Eager(Type<'db>), +} + +impl<'db> From> for TypeVarDefaultEvaluation<'db> { + fn from(value: Type<'db>) -> Self { + TypeVarDefaultEvaluation::Eager(value) + } +} + +/// Whether a typevar bound/constraints is eagerly specified or lazily evaluated. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum TypeVarBoundOrConstraintsEvaluation<'db> { + /// There is a lazily-evaluated upper bound. + LazyUpperBound, + /// There is a lazily-evaluated set of constraints. + LazyConstraints, + /// The upper bound/constraints are eagerly specified. + Eager(TypeVarBoundOrConstraints<'db>), +} + +impl<'db> From> for TypeVarBoundOrConstraintsEvaluation<'db> { + fn from(value: TypeVarBoundOrConstraints<'db>) -> Self { + TypeVarBoundOrConstraintsEvaluation::Eager(value) + } +} + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub enum TypeVarBoundOrConstraints<'db> { UpperBound(Type<'db>), diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index d517caa887..efaf549680 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -397,7 +397,7 @@ impl<'db> Bindings<'db> { } Some("__default__") => { overload.set_return_type( - typevar.default_ty(db).unwrap_or_else(|| { + typevar.default_type(db).unwrap_or_else(|| { KnownClass::NoDefaultType.to_instance(db) }), ); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 72e8d20d0b..f8f1087a24 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -4520,7 +4520,9 @@ impl KnownClass { } let bound_or_constraint = match (bound, constraints) { - (Some(bound), None) => Some(TypeVarBoundOrConstraints::UpperBound(*bound)), + (Some(bound), None) => { + Some(TypeVarBoundOrConstraints::UpperBound(*bound).into()) + } (None, Some(_constraints)) => { // We don't use UnionType::from_elements or UnionBuilder here, @@ -4536,7 +4538,7 @@ impl KnownClass { .map(|(_, ty)| ty) .collect::>(), ); - Some(TypeVarBoundOrConstraints::Constraints(elements)) + Some(TypeVarBoundOrConstraints::Constraints(elements).into()) } // TODO: Emit a diagnostic that TypeVar cannot be both bounded and @@ -4554,7 +4556,7 @@ impl KnownClass { Some(containing_assignment), bound_or_constraint, variance, - *default, + default.map(Into::into), TypeVarKind::Legacy, ), ))); diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index db8c67f8f3..b50ca3d996 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -14,9 +14,8 @@ use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::tuple::TupleSpec; use crate::types::{ - BoundTypeVarInstance, CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, - StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, - UnionType, WrapperDescriptorKind, + CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType, + SubclassOfInner, Type, UnionType, WrapperDescriptorKind, }; impl<'db> Type<'db> { @@ -417,83 +416,6 @@ impl Display for DisplayGenericAlias<'_> { } } -impl<'db> TypeVarInstance<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayTypeVarInstance<'db> { - DisplayTypeVarInstance { typevar: self, db } - } -} - -pub(crate) struct DisplayTypeVarInstance<'db> { - typevar: TypeVarInstance<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayTypeVarInstance<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - display_quoted_string(self.typevar.name(self.db)).fmt(f)?; - match self.typevar.bound_or_constraints(self.db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - write!(f, ", bound={}", bound.display(self.db))?; - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - for constraint in constraints.iter(self.db) { - write!(f, ", {}", constraint.display(self.db))?; - } - } - None => {} - } - if let Some(default_type) = self.typevar.default_ty(self.db) { - write!(f, ", default={}", default_type.display(self.db))?; - } - Ok(()) - } -} - -impl<'db> BoundTypeVarInstance<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayBoundTypeVarInstance<'db> { - DisplayBoundTypeVarInstance { - bound_typevar: self, - db, - } - } -} - -pub(crate) struct DisplayBoundTypeVarInstance<'db> { - bound_typevar: BoundTypeVarInstance<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayBoundTypeVarInstance<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - // This looks very much like DisplayTypeVarInstance::fmt, but note that we have typevar - // default values in a subtly different way: if the default value contains other typevars, - // here those must be bound as well, whereas in DisplayTypeVarInstance they should not. See - // BoundTypeVarInstance::default_ty for more details. - let typevar = self.bound_typevar.typevar(self.db); - f.write_str(typevar.name(self.db))?; - match typevar.bound_or_constraints(self.db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - write!(f, ": {}", bound.display(self.db))?; - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - f.write_str(": (")?; - for (idx, constraint) in constraints.iter(self.db).enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - constraint.display(self.db).fmt(f)?; - } - f.write_char(')')?; - } - None => {} - } - if let Some(default_type) = self.bound_typevar.default_ty(self.db) { - write!(f, " = {}", default_type.display(self.db))?; - } - Ok(()) - } -} - impl<'db> GenericContext<'db> { pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> { DisplayGenericContext { @@ -545,7 +467,7 @@ impl Display for DisplayGenericContext<'_> { if idx > 0 { f.write_str(", ")?; } - bound_typevar.display(self.db).fmt(f)?; + f.write_str(bound_typevar.typevar(self.db).name(self.db))?; } f.write_char(']') } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index c846ca6ee2..00089be337 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -225,7 +225,7 @@ impl<'db> GenericContext<'db> { } None => {} } - if let Some(default_ty) = bound_typevar.default_ty(db) { + if let Some(default_ty) = bound_typevar.default_type(db) { parameter = parameter.with_default_type(default_ty); } parameter @@ -337,7 +337,7 @@ impl<'db> GenericContext<'db> { continue; } - let Some(default) = typevar.default_ty(db) else { + let Some(default) = typevar.default_type(db) else { continue; }; @@ -756,7 +756,7 @@ impl<'db> SpecializationBuilder<'db> { self.types .get(variable) .copied() - .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) + .unwrap_or(variable.default_type(self.db).unwrap_or(Type::unknown())) }) .collect(); // TODO Infer the tuple spec for a tuple type diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f8bf887e0f..8bba369c3b 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -121,8 +121,9 @@ use crate::types::{ IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, Truthiness, Type, TypeAliasType, - TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type, + TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, + TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, + UnionType, binding_type, todo_type, }; use crate::unpack::{EvaluationMode, Unpack, UnpackPosition}; use crate::util::diagnostics::format_enumeration; @@ -1748,6 +1749,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { DefinitionKind::Class(class) => { self.infer_class_deferred(definition, class.node(self.module())); } + DefinitionKind::TypeVar(typevar) => { + self.infer_typevar_deferred(typevar.node(self.module())); + } _ => {} } } @@ -2243,6 +2247,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_type_parameters(type_params); if let Some(arguments) = class.arguments.as_deref() { + let in_stub = self.in_stub(); + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, in_stub.into()); let mut call_arguments = CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| { let ty = self.infer_expression(splatted_value); @@ -2251,6 +2258,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); + self.deferred_state = previous_deferred_state; } self.typevar_binding_context = previous_typevar_binding_context; @@ -2271,7 +2279,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.typevar_binding_context.replace(binding_context); self.infer_return_type_annotation( function.returns.as_deref(), - DeferredExpressionState::None, + self.defer_annotations().into(), ); self.infer_type_parameters(type_params); self.infer_parameters(&function.parameters); @@ -2316,7 +2324,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class_scope = match parent_scope.kind() { ScopeKind::Class => parent_scope, - ScopeKind::Annotation => { + ScopeKind::TypeParams => { let class_scope_id = parent_scope.parent()?; let potentially_class_scope = self.index.scope(class_scope_id); @@ -2757,7 +2765,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let annotated = self.infer_optional_annotation_expression( parameter.annotation.as_deref(), - DeferredExpressionState::None, + self.defer_annotations().into(), ); if let Some(qualifiers) = annotated.map(|annotated| annotated.qualifiers) { @@ -2792,7 +2800,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_optional_annotation_expression( annotation.as_deref(), - DeferredExpressionState::None, + self.defer_annotations().into(), ); } @@ -3402,44 +3410,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { builder.into_diagnostic("TypeVar must have at least two constrained types"); } - self.infer_expression(expr); None } else { - // We don't use UnionType::from_elements or UnionBuilder here, because we don't - // want to simplify the list of constraints like we do with the elements of an - // actual union type. - // TODO: Consider using a new `OneOfType` connective here instead, since that - // more accurately represents the actual semantics of typevar constraints. - let elements = UnionType::new( - self.db(), - elts.iter() - .map(|expr| self.infer_type_expression(expr)) - .collect::>(), - ); - let constraints = TypeVarBoundOrConstraints::Constraints(elements); - // But when we construct an actual union type for the constraint expression as - // a whole, we do use UnionType::from_elements to maintain the invariant that - // all union types are simplified. - self.store_expression_type( - expr, - UnionType::from_elements(self.db(), elements.elements(self.db())), - ); - Some(constraints) + Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints) } } - Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound( - self.infer_type_expression(expr), - )), + Some(_) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), None => None, }; - let default_ty = self.infer_optional_type_expression(default.as_deref()); + if bound_or_constraint.is_some() || default.is_some() { + self.deferred.insert(definition); + } let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( self.db(), &name.id, Some(definition), bound_or_constraint, TypeVarVariance::Invariant, // TODO: infer this - default_ty, + default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy), TypeVarKind::Pep695, ))); self.add_declaration_with_binding( @@ -3449,6 +3437,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } + fn infer_typevar_deferred(&mut self, node: &ast::TypeParamTypeVar) { + let ast::TypeParamTypeVar { + range: _, + node_index: _, + name: _, + bound, + default, + } = node; + match bound.as_deref() { + Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { + // We don't use UnionType::from_elements or UnionBuilder here, because we don't + // want to simplify the list of constraints like we do with the elements of an + // actual union type. + // TODO: Consider using a new `OneOfType` connective here instead, since that + // more accurately represents the actual semantics of typevar constraints. + let ty = Type::Union(UnionType::new( + self.db(), + elts.iter() + .map(|expr| self.infer_type_expression(expr)) + .collect::>(), + )); + self.store_expression_type(expr, ty); + } + Some(expr) => { + self.infer_type_expression(expr); + } + None => {} + } + self.infer_optional_type_expression(default.as_deref()); + } + fn infer_paramspec_definition( &mut self, node: &ast::TypeParamParamSpec, @@ -6573,7 +6592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let place_table = self.index.place_table(file_scope_id); let use_def = self.index.use_def_map(file_scope_id); - // If we're inferring types of deferred expressions, always treat them as public symbols + // If we're inferring types of deferred expressions, look them up from end-of-scope. if self.is_deferred() { let place = if let Some(place_id) = place_table.place_id(expr) { place_from_bindings(db, use_def.all_reachable_bindings(place_id)) @@ -6684,11 +6703,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Class scopes are not visible to nested scopes, and we need to handle global // scope differently (because an unbound name there falls back to builtins), so // check only function-like scopes. - // There is one exception to this rule: type parameter scopes can see + // There is one exception to this rule: annotation scopes can see // names defined in an immediately-enclosing class scope. let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file); - let is_immediately_enclosing_scope = scope.is_type_parameter(db) + let is_immediately_enclosing_scope = scope.is_annotation(db) && scope .scope(db) .parent() @@ -9535,17 +9554,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty } - /// Similar to [`infer_type_expression`], but accepts an optional type expression and returns - /// [`None`] if the expression is [`None`]. - /// - /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression - fn infer_optional_type_expression( - &mut self, - expression: Option<&ast::Expr>, - ) -> Option> { - expression.map(|expr| self.infer_type_expression(expr)) - } - /// Similar to [`infer_type_expression`], but accepts a [`DeferredExpressionState`]. /// /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression @@ -9560,6 +9568,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { annotation_ty } + /// Similar to [`infer_type_expression`], but accepts an optional expression. + /// + /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression_with_state + fn infer_optional_type_expression( + &mut self, + expression: Option<&ast::Expr>, + ) -> Option> { + expression.map(|expr| self.infer_type_expression(expr)) + } + fn report_invalid_type_expression( &self, expression: &ast::Expr, @@ -11541,38 +11559,20 @@ mod tests { ); assert_eq!( typevar - .default_ty(&db) + .default_type(&db) .map(|ty| ty.display(&db).to_string()), default.map(std::borrow::ToOwned::to_owned) ); }; - check_typevar("T", "typing.TypeVar(\"T\")", None, None, None); - check_typevar("U", "typing.TypeVar(\"U\", bound=A)", Some("A"), None, None); - check_typevar( - "V", - "typing.TypeVar(\"V\", A, B)", - None, - Some(&["A", "B"]), - None, - ); - check_typevar( - "W", - "typing.TypeVar(\"W\", default=A)", - None, - None, - Some("A"), - ); - check_typevar( - "X", - "typing.TypeVar(\"X\", bound=A, default=A1)", - Some("A"), - None, - Some("A1"), - ); + check_typevar("T", "typing.TypeVar", None, None, None); + check_typevar("U", "typing.TypeVar", Some("A"), None, None); + check_typevar("V", "typing.TypeVar", None, Some(&["A", "B"]), None); + check_typevar("W", "typing.TypeVar", None, None, Some("A")); + check_typevar("X", "typing.TypeVar", Some("A"), None, Some("A1")); // a typevar with less than two constraints is treated as unconstrained - check_typevar("Y", "typing.TypeVar(\"Y\")", None, None, None); + check_typevar("Y", "typing.TypeVar", None, None, None); } /// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 2120c62548..11ca44bb8f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1797,11 +1797,7 @@ mod tests { }; assert_eq!(a_name, "a"); assert_eq!(b_name, "b"); - // TODO resolution should not be deferred; we should see A, not A | B - assert_eq!( - a_annotated_ty.unwrap().display(&db).to_string(), - "Unknown | A | B" - ); + assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "A"); assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T@f"); } diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index f1815f50a5..1a67759432 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -97,9 +97,12 @@ impl<'db> SubclassOfType<'db> { db, Name::new_static("T_all"), None, - Some(TypeVarBoundOrConstraints::UpperBound( - KnownClass::Type.to_instance(db), - )), + Some( + TypeVarBoundOrConstraints::UpperBound( + KnownClass::Type.to_instance(db), + ) + .into(), + ), variance, None, TypeVarKind::Pep695,