diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 9ca4ce01da721..38d54a65f9112 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L95) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L96) ## `conflicting-argument-forms` @@ -83,7 +83,7 @@ f(int) # error ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L139) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L140) ## `conflicting-declarations` @@ -113,7 +113,7 @@ a = 1 ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L165) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L166) ## `conflicting-metaclass` @@ -144,7 +144,7 @@ class C(A, B): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L190) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L191) ## `cyclic-class-definition` @@ -175,7 +175,7 @@ class B(A): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L216) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L217) ## `duplicate-base` @@ -201,7 +201,7 @@ class B(A, A): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261) ## `duplicate-kw-only` @@ -238,7 +238,7 @@ class A: # Crash at runtime ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L281) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282) ## `escape-character-in-forward-annotation` @@ -375,7 +375,7 @@ TypeError: multiple bases have instance lay-out conflict ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L313) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L314) ## `inconsistent-mro` @@ -404,7 +404,7 @@ class C(A, B): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L400) ## `index-out-of-bounds` @@ -429,7 +429,7 @@ t[3] # IndexError: tuple index out of range ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L423) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L424) ## `invalid-argument-type` @@ -455,7 +455,7 @@ func("foo") # error: [invalid-argument-type] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L443) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L444) ## `invalid-assignment` @@ -482,7 +482,7 @@ a: int = '' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L483) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L484) ## `invalid-attribute-access` @@ -515,7 +515,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1487) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1488) ## `invalid-base` @@ -538,7 +538,7 @@ class A(42): ... # error: [invalid-base] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L505) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506) ## `invalid-context-manager` @@ -564,7 +564,7 @@ with 1: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L556) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L557) ## `invalid-declaration` @@ -592,7 +592,7 @@ a: str ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L577) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L578) ## `invalid-exception-caught` @@ -633,7 +633,7 @@ except ZeroDivisionError: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L600) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L601) ## `invalid-generic-class` @@ -664,7 +664,7 @@ class C[U](Generic[T]): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L636) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637) ## `invalid-legacy-type-variable` @@ -697,7 +697,7 @@ def f(t: TypeVar("U")): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L662) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663) ## `invalid-metaclass` @@ -729,7 +729,7 @@ class B(metaclass=f): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L711) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712) ## `invalid-overload` @@ -777,7 +777,7 @@ def foo(x: int) -> int: ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L738) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739) ## `invalid-parameter-default` @@ -802,7 +802,7 @@ def f(a: int = ''): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L781) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L782) ## `invalid-protocol` @@ -835,7 +835,7 @@ TypeError: Protocols can only inherit from other protocols, got ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L371) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L372) ## `invalid-raise` @@ -883,7 +883,7 @@ def g(): ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L801) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L802) ## `invalid-return-type` @@ -907,7 +907,7 @@ def func() -> int: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L464) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L465) ## `invalid-super-argument` @@ -951,7 +951,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)` ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L844) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L845) ## `invalid-syntax-in-forward-annotation` @@ -991,7 +991,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L691) ## `invalid-type-checking-constant` @@ -1020,7 +1020,7 @@ TYPE_CHECKING = '' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L883) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L884) ## `invalid-type-form` @@ -1049,7 +1049,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L907) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L908) ## `invalid-type-guard-call` @@ -1082,7 +1082,7 @@ f(10) # Error ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960) ## `invalid-type-guard-definition` @@ -1115,7 +1115,7 @@ class C: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L931) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932) ## `invalid-type-variable-constraints` @@ -1149,7 +1149,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L987) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L988) ## `missing-argument` @@ -1173,7 +1173,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1016) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1017) ## `no-matching-overload` @@ -1201,7 +1201,7 @@ func("string") # error: [no-matching-overload] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1035) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036) ## `non-subscriptable` @@ -1224,7 +1224,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1058) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1059) ## `not-iterable` @@ -1249,7 +1249,7 @@ for i in 34: # TypeError: 'int' object is not iterable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1076) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1077) ## `parameter-already-assigned` @@ -1275,7 +1275,7 @@ f(1, x=2) # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1127) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1128) ## `raw-string-type-annotation` @@ -1334,7 +1334,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1464) ## `subclass-of-final-class` @@ -1362,7 +1362,7 @@ class B(A): ... # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1218) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219) ## `too-many-positional-arguments` @@ -1388,7 +1388,7 @@ f("foo") # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1263) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1264) ## `type-assertion-failure` @@ -1415,7 +1415,7 @@ def _(x: int): ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1241) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242) ## `unavailable-implicit-super-arguments` @@ -1459,7 +1459,7 @@ class A: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1284) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1285) ## `unknown-argument` @@ -1485,7 +1485,7 @@ f(x=1, y=2) # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1341) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1342) ## `unresolved-attribute` @@ -1512,7 +1512,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1362) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1363) ## `unresolved-import` @@ -1536,7 +1536,7 @@ import foo # ModuleNotFoundError: No module named 'foo' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1384) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1385) ## `unresolved-reference` @@ -1560,7 +1560,7 @@ print(x) # NameError: name 'x' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1403) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1404) ## `unsupported-bool-conversion` @@ -1596,7 +1596,7 @@ b1 < b2 < b1 # exception raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1096) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1097) ## `unsupported-operator` @@ -1623,7 +1623,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1422) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1423) ## `zero-stepsize-in-slice` @@ -1647,7 +1647,7 @@ l[1:10:0] # ValueError: slice step cannot be zero ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1444) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1445) ## `invalid-ignore-comment` @@ -1703,7 +1703,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1148) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1149) ## `possibly-unbound-implicit-call` @@ -1734,7 +1734,7 @@ A()[0] # TypeError: 'A' object is not subscriptable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L114) ## `possibly-unbound-import` @@ -1765,7 +1765,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1170) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171) ## `redundant-cast` @@ -1791,7 +1791,7 @@ cast(int, f()) # Redundant ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516) ## `undefined-reveal` @@ -1814,7 +1814,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1324) ## `unknown-rule` @@ -1882,7 +1882,7 @@ class D(C): ... # error: [unsupported-base] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L524) ## `division-by-zero` @@ -1905,7 +1905,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L242) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L243) ## `possibly-unresolved-reference` @@ -1932,7 +1932,7 @@ print(x) # NameError: name 'x' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197) ## `unused-ignore-comment` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 91d945436405f..12183c5b2854b 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -58,7 +58,7 @@ reveal_type(c) # revealed: tuple[str, int] reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] reveal_type(e) # revealed: tuple[str, ...] -reveal_type(f) # revealed: @Todo(PEP 646) +reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes] reveal_type(g) # revealed: @Todo(PEP 646) reveal_type(h) # revealed: tuple[list[int], list[int]] diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 53643da8ecdb0..993bf6459419c 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1722,7 +1722,7 @@ d = True reveal_type(d.__class__) # revealed: e = (42, 42) -reveal_type(e.__class__) # revealed: +reveal_type(e.__class__) # revealed: def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): reveal_type(a.__class__) # revealed: type[int] diff --git a/crates/ty_python_semantic/resources/mdtest/binary/tuples.md b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md index 78279831fd4ca..947262fc691ab 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md @@ -17,6 +17,32 @@ def _(x: tuple[int, str], y: tuple[None, tuple[int]]): ```py def _(x: tuple[int, ...], y: tuple[str, ...]): + reveal_type(x + x) # revealed: tuple[int, ...] reveal_type(x + y) # revealed: tuple[int | str, ...] - reveal_type(x + (1, 2)) # revealed: tuple[int, ...] + reveal_type((1, 2) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]] + reveal_type(x + (3, 4)) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]] + reveal_type((1, 2) + x + (3, 4)) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]] + reveal_type((1, 2) + y + (3, 4) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]] +``` + +We get the same results even when we use a legacy type alias, even though this involves first +inferring the `tuple[...]` expression as a value form. (Doing so gives a generic alias of the +`tuple` type, but as a special case, we include the full detailed tuple element specification in +specializations of `tuple`.) + +```py +from typing import Literal + +OneTwo = tuple[Literal[1], Literal[2]] +ThreeFour = tuple[Literal[3], Literal[4]] +IntTuple = tuple[int, ...] +StrTuple = tuple[str, ...] + +def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour): + reveal_type(x + x) # revealed: tuple[int, ...] + reveal_type(x + y) # revealed: tuple[int | str, ...] + reveal_type(one_two + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]] + reveal_type(x + three_four) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]] + reveal_type(one_two + x + three_four) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]] + reveal_type(one_two + y + three_four + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 651de8c08368a..da947217f8a31 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -130,6 +130,44 @@ reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str ``` +## Inferring tuple parameter types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypeVar + +T = TypeVar("T") + +def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T: + return x[-2] + +# TODO: revealed: Literal[True] +reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T: + return x[1] + +# TODO: revealed: Literal[b"foo"] +reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_fixed_tuple(x: tuple[T, int]) -> T: + return x[0] + +reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True] + +def takes_homogeneous_tuple(x: tuple[T, ...]) -> T: + return x[0] + +# TODO: revealed: Literal[42] +reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown +# TODO: revealed: Literal[42, 43] +reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown +``` + ## Inferring a bound typevar diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index c9357ecc26684..bc7f96c0e4653 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -125,6 +125,35 @@ reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str ``` +## Inferring tuple parameter types + +```py +def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T: + return x[-2] + +# TODO: revealed: Literal[True] +reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T: + return x[1] + +# TODO: revealed: Literal[b"foo"] +reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_fixed_tuple[T](x: tuple[T, int]) -> T: + return x[0] + +reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True] + +def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T: + return x[0] + +# TODO: revealed: Literal[42] +reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown +# TODO: revealed: Literal[42, 43] +reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown +``` + ## Inferring a bound typevar diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md index c74f24a0aa3b4..934fd3f605bbf 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md @@ -192,10 +192,9 @@ def _(t1: tuple[int | None, int | None], t2: tuple[int, int] | tuple[None, None] reveal_type(t1[1]) # revealed: int | None if t2[0] is not None: + reveal_type(t2[0]) # revealed: int # TODO: should be int - reveal_type(t2[0]) # revealed: Unknown & ~None - # TODO: should be int - reveal_type(t2[1]) # revealed: Unknown + reveal_type(t2[1]) # revealed: int | None ``` ### String subscript diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index da19b569488fc..e998f00966275 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -215,12 +215,12 @@ def _(a: tuple[str, int] | tuple[int, str], c: C[Any]): # TODO: Should be `tuple[int, str]` reveal_type(a) # revealed: tuple[str, int] | tuple[int, str] # TODO: Should be `str` - reveal_type(a[1]) # revealed: Unknown + reveal_type(a[1]) # revealed: str | int if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]] # TODO: Should be `tuple[int, str]` reveal_type(a) # revealed: tuple[str, int] | tuple[int, str] - reveal_type(a[0]) # revealed: Unknown & int + reveal_type(a[0]) # revealed: int # TODO: Should be `TypeGuard[str @ c.v]` if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form) diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index c286b86e02f84..49ef6f6f37fc6 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -69,8 +69,64 @@ def _(m: int, n: int): t[::0] # error: [zero-stepsize-in-slice] tuple_slice = t[m:n] - # TODO: Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` - reveal_type(tuple_slice) # revealed: tuple[Unknown, ...] + reveal_type(tuple_slice) # revealed: tuple[Literal[1, "a", b"b"] | None, ...] +``` + +## Slices of homogeneous and mixed tuples + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Literal + +def homogeneous(t: tuple[str, ...]) -> None: + reveal_type(t[0]) # revealed: str + reveal_type(t[1]) # revealed: str + reveal_type(t[2]) # revealed: str + reveal_type(t[3]) # revealed: str + + reveal_type(t[-1]) # revealed: str + reveal_type(t[-2]) # revealed: str + reveal_type(t[-3]) # revealed: str + reveal_type(t[-4]) # revealed: str + +def mixed(s: tuple[str, ...]) -> None: + t = (1, 2, 3) + s + (8, 9, 10) + + reveal_type(t[0]) # revealed: Literal[1] + reveal_type(t[1]) # revealed: Literal[2] + reveal_type(t[2]) # revealed: Literal[3] + reveal_type(t[3]) # revealed: str | Literal[8] + reveal_type(t[4]) # revealed: str | Literal[8, 9] + reveal_type(t[5]) # revealed: str | Literal[8, 9, 10] + + reveal_type(t[-1]) # revealed: Literal[10] + reveal_type(t[-2]) # revealed: Literal[9] + reveal_type(t[-3]) # revealed: Literal[8] + reveal_type(t[-4]) # revealed: Literal[3] | str + reveal_type(t[-5]) # revealed: Literal[2, 3] | str + reveal_type(t[-6]) # revealed: Literal[1, 2, 3] | str +``` + +## `tuple` as generic alias + +For tuple instances, we can track more detailed information about the length and element types of +the tuple. This information carries over to the generic alias that the tuple is an instance of. + +```py +def _(a: tuple, b: tuple[int], c: tuple[int, str], d: tuple[int, ...]) -> None: + reveal_type(a) # revealed: tuple[Unknown, ...] + reveal_type(b) # revealed: tuple[int] + reveal_type(c) # revealed: tuple[int, str] + reveal_type(d) # revealed: tuple[int, ...] + +reveal_type(tuple) # revealed: +reveal_type(tuple[int]) # revealed: +reveal_type(tuple[int, str]) # revealed: +reveal_type(tuple[int, ...]) # revealed: ``` ## Inheritance @@ -83,8 +139,13 @@ python-version = "3.9" ```py class A(tuple[int, str]): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] reveal_type(A.__mro__) + +class C(tuple): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(C.__mro__) ``` ## `typing.Tuple` @@ -109,9 +170,19 @@ def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]): Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself is not a class. +```toml +[environment] +python-version = "3.9" +``` + ```py from typing import Tuple +class A(Tuple[int, str]): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(A.__mro__) + class C(Tuple): ... # revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index 350ee0123e906..f1436a7c4815e 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -16,6 +16,28 @@ def _(p: P, q: Q): assert_type((p, q), tuple[P, Q]) ``` +## Instantiating tuples + +Like all classes, tuples can be instantiated by invoking the `tuple` class. When instantiating a +specialization of `tuple` we (TODO: should) check that the values passed in match the element types +defined in the specialization. + +```py +# TODO: revealed: tuple[()] +reveal_type(tuple()) # revealed: tuple[Unknown, ...] +# TODO: revealed: tuple[Literal[1]] +reveal_type(tuple([1])) # revealed: tuple[Unknown, ...] +reveal_type(tuple[int]([1])) # revealed: tuple[int] +# TODO: error for invalid arguments +reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str] + +reveal_type(().__class__()) # revealed: tuple[()] +# TODO: error for invalid arguments +reveal_type((1,).__class__()) # revealed: tuple[Literal[1]] +# TODO: error for invalid arguments +reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]] +``` + ## Subtyping relationships The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1` @@ -60,10 +82,7 @@ class AnotherEmptyTuple(tuple[()]): ... static_assert(not is_equivalent_to(AnotherEmptyTuple, tuple[()])) -# TODO: These should not be errors -# error: [static-assert-error] static_assert(is_subtype_of(AnotherEmptyTuple, tuple[()])) -# error: [static-assert-error] static_assert(is_assignable_to(AnotherEmptyTuple, tuple[()])) ``` @@ -158,8 +177,6 @@ class NotAlwaysTruthyTuple(tuple[int]): def __bool__(self) -> bool: return False -# TODO: This assignment should be allowed -# error: [invalid-assignment] t: tuple[int] = NotAlwaysTruthyTuple((1,)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index b67c74d6934e7..6b96210b54a63 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -303,6 +303,11 @@ static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str])) ## Assignability of heterogeneous tuple types to homogeneous tuple types +```toml +[environment] +python-version = "3.12" +``` + While a homogeneous tuple type is not assignable to any heterogeneous tuple types, a heterogeneous tuple type can be assignable to a homogeneous tuple type, and homogeneous tuple types can be assignable to `Sequence`: @@ -312,6 +317,11 @@ from typing import Literal, Any, Sequence from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[Literal[2], ...]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[*tuple[Literal[1], ...], Literal[2]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[str, ...], Literal[2]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[2], *tuple[str, ...]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[*tuple[str, ...], Literal[1], Literal[2]])) static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, ...])) static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int | str, ...])) static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Any, ...])) @@ -330,6 +340,218 @@ static_assert(is_assignable_to(tuple[()], Sequence[int])) static_assert(not is_assignable_to(tuple[int, int], tuple[str, ...])) ``` +## Assignability of two mixed tuple types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, Any, Sequence +from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy + +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...]], + ) +) + +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...]], + ) +) + +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...]], + ) +) + +static_assert( + not is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_assignable_to( + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[Literal[1], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[Literal[1], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_assignable_to( + tuple[*tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[*tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[*tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +``` + +## Assignability of the gradual tuple + +```toml +[environment] +python-version = "3.12" +``` + +As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, which +is assignable to every tuple of any length. + +```py +from typing import Any +from ty_extensions import static_assert, is_assignable_to + +static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[Any])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[int, ...])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[int])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[int, int])) +``` + +This also applies when `tuple[Any, ...]` is unpacked into a mixed tuple. + +```py +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[int, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, ...])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, int])) + +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[*tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, ...])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, int])) + +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, int])) +``` + +The same is not true of fully static tuple types, since an unbounded homogeneous tuple is defined to +be the _union_ of all tuple lengths, not the _gradual choice_ of them. + +```py +static_assert(is_assignable_to(tuple[int, ...], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[Any])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, ...], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[int])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[int, int])) + +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[int, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, int])) + +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[*tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[int])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[int, int])) + +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, int])) +``` + ## Union types ```py @@ -828,8 +1050,8 @@ sets of possible materializations -- if they represent the same sets of possible sets of sets of possible runtime objects). By this principle `int | Any` is gradually equivalent to `Unknown | int`, since they have exactly the same sets of posisble materializations. But `bool | Any` is not equivalent to `int`, since there are many possible materializations of -`bool | Any` that are not assignable to `int`. It is therefore *not* necessary for `X` to be -gradually equivalent to `Y` in order for `Foo[X]` to be assignable to `Foo[Y]`; it is *only* +`bool | Any` that are not assignable to `int`. It is therefore _not_ necessary for `X` to be +gradually equivalent to `Y` in order for `Foo[X]` to be assignable to `Foo[Y]`; it is _only_ necessary for `X` and `Y` to be mutually assignable. ```py @@ -887,4 +1109,6 @@ static_assert(not is_assignable_to(TypeGuard[Unknown], str)) # error: [static-a static_assert(not is_assignable_to(TypeIs[Any], str)) ``` +[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form +[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md index ef9f677c55eed..53bf7b6cf4637 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md @@ -48,8 +48,7 @@ static_assert(not is_fully_static(Any | str)) static_assert(not is_fully_static(str | Unknown)) static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]])) -# TODO: should pass -static_assert(not is_fully_static(tuple[Any, ...])) # error: [static-assert-error] +static_assert(not is_fully_static(tuple[Any, ...])) static_assert(not is_fully_static(tuple[int, Any])) static_assert(not is_fully_static(type[Any])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index a77022df7a9d1..ba445907ff40d 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -159,6 +159,11 @@ from typing import Literal, Any, Sequence from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[Literal[2], ...]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[*tuple[Literal[1], ...], Literal[2]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[str, ...], Literal[2]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[2], *tuple[str, ...]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[*tuple[str, ...], Literal[1], Literal[2]])) static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int, ...])) static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int | str, ...])) static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Not[AlwaysFalsy], ...])) @@ -177,6 +182,215 @@ static_assert(not is_subtype_of(tuple[int, ...], Sequence[Any])) static_assert(not is_subtype_of(tuple[Any, ...], Sequence[int])) ``` +## Subtyping of two mixed tuple types + +```py +from typing import Literal, Any, Sequence +from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy + +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...]], + ) +) + +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...]], + ) +) + +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...]], + ) +) + +static_assert( + not is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_subtype_of( + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +``` + +## Subtyping of the gradual tuple + +```toml +[environment] +python-version = "3.12" +``` + +As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type. +However, the special-case behavior of assignability does not also apply to subtyping, since gradual +types to not participate in subtyping. + +```py +from typing import Any +from ty_extensions import static_assert, is_subtype_of + +static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[int])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, int])) +``` + +Subtyping also does not apply when `tuple[Any, ...]` is unpacked into a mixed tuple. + +```py +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[int, ...]])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[*tuple[int, ...], int])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, int])) +``` + +Subtyping does apply to unbounded homogeneous tuples of a fully static type. However, such tuples +are defined to be the _union_ of all tuple lengths, not the _gradual choice_ of them, so no +variable-length tuples are a subtyping of _any_ fixed-length tuple. + +```py +static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[int, ...], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[int])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, *tuple[int, ...]])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[*tuple[int, ...], int], tuple[*tuple[int, ...], int])) +static_assert(is_subtype_of(tuple[*tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, int])) +``` + ## Union types ```py @@ -1639,5 +1853,7 @@ static_assert(is_subtype_of(CallableTypeOf[overload_ab], CallableTypeOf[overload static_assert(is_subtype_of(CallableTypeOf[overload_ba], CallableTypeOf[overload_ab])) ``` +[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form +[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form [special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index f92289d6ec91f..f947eda2625a0 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -7,6 +7,7 @@ cpython # too many cycle iterations hydpy # too many iterations ibis # too many iterations jax # too many iterations +mypy # too many iterations (self-recursive type alias) packaging # too many iterations pandas # slow (9s) pandera # stack overflow diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index be085564974b1..a6e774fbada76 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -60,7 +60,6 @@ mkdocs mkosi mongo-python-driver more-itertools -mypy mypy-protobuf mypy_primer nionutils diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4a28fe40c86ca..45db3b20bac85 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -51,6 +51,7 @@ use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, Parameters}; +use crate::types::tuple::{TupleSpec, TupleType}; pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic; use crate::{Db, FxOrderSet, Module, Program}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; @@ -78,6 +79,7 @@ mod slots; mod special_form; mod string_annotation; mod subclass_of; +mod tuple; mod type_ordering; mod unpacker; @@ -543,8 +545,10 @@ pub enum Type<'db> { LiteralString, /// A bytes literal BytesLiteral(BytesLiteralType<'db>), - /// A heterogeneous tuple type, with elements of the given types in source order. - // TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`. + /// An instance of the builtin `tuple` class. + /// TODO: Consider removing this in favor of `NominalInstance`. This is currently stored as a + /// separate variant partly for historical reasons, and partly to allow us to easily + /// distinguish tuples since they occur so often. Tuple(TupleType<'db>), /// An instance of a typevar in a generic class or function. When the generic class or function /// is specialized, we will replace this typevar with its specialization. @@ -720,13 +724,7 @@ impl<'db> Type<'db> { .map(|ty| ty.materialize(db, variance.flip())), ) .build(), - Type::Tuple(tuple_type) => TupleType::from_elements( - db, - tuple_type - .elements(db) - .iter() - .map(|ty| ty.materialize(db, variance)), - ), + Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.materialize(db, variance)), Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)), Type::TypeIs(type_is) => { type_is.with_type(db, type_is.return_type(db).materialize(db, variance)) @@ -770,8 +768,8 @@ impl<'db> Type<'db> { Self::Tuple(tuple) => TupleType::from_elements( db, tuple - .elements(db) - .iter() + .tuple(db) + .all_elements() .map(|ty| ty.replace_self_reference(db, class)), ), @@ -881,8 +879,8 @@ impl<'db> Type<'db> { } Self::Tuple(tuple) => tuple - .elements(db) - .iter() + .tuple(db) + .all_elements() .any(|ty| ty.any_over_type(db, type_fn)), Self::Union(union) => union @@ -1076,13 +1074,6 @@ impl<'db> Type<'db> { .expect("Expected a Type::IntLiteral variant") } - pub const fn into_tuple(self) -> Option> { - match self { - Type::Tuple(tuple_type) => Some(tuple_type), - _ => None, - } - } - pub const fn is_boolean_literal(&self) -> bool { matches!(self, Type::BooleanLiteral(..)) } @@ -1141,7 +1132,7 @@ impl<'db> Type<'db> { match self { Type::Union(union) => Type::Union(union.normalized(db)), Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), - Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), + Type::Tuple(tuple) => Type::tuple(db, tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), Type::ProtocolInstance(protocol) => protocol.normalized(db), Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)), @@ -1441,27 +1432,23 @@ impl<'db> Type<'db> { false } - // A fully static heterogeneous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B` - // iff the two tuple types have the same number of elements and each element-type in `A` is a subtype - // of the element-type at the same index in `B`. (Now say that 5 times fast.) - // - // For example: `tuple[bool, bool]` is a subtype of `tuple[int, int]`, - // but `tuple[bool, bool, bool]` is not a subtype of `tuple[int, int]` (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { - let self_elements = self_tuple.elements(db); - let target_elements = target_tuple.elements(db); - self_elements.len() == target_elements.len() - && self_elements.iter().zip(target_elements).all( - |(self_element, target_element)| { - self_element.has_relation_to(db, *target_element, relation) - }, - ) + self_tuple.has_relation_to(db, target_tuple, relation) } - // `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]` - (Type::Tuple(tuple), _) => tuple - .homogeneous_supertype(db) - .has_relation_to(db, target, relation), + (Type::Tuple(self_tuple), Type::NominalInstance(target_instance)) => { + self_tuple.to_class_type(db).is_some_and(|self_class| { + self_class.has_relation_to(db, target_instance.class, relation) + }) + } + (Type::NominalInstance(self_instance), Type::Tuple(target_tuple)) => { + target_tuple.to_class_type(db).is_some_and(|target_class| { + self_instance + .class + .has_relation_to(db, target_class, relation) + }) + } + (Type::Tuple(_), _) => false, (Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target), (Type::BoundSuper(_), _) => KnownClass::Super @@ -1961,14 +1948,15 @@ impl<'db> Type<'db> { !known_instance.is_instance_of(db, instance.class) } - ( - known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)), - Type::Tuple(tuple), - ) - | ( - Type::Tuple(tuple), - known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)), - ) => known_instance_ty.is_disjoint_from(db, tuple.homogeneous_supertype(db)), + (Type::SpecialForm(special_form), Type::Tuple(tuple)) + | (Type::Tuple(tuple), Type::SpecialForm(special_form)) => tuple + .to_class_type(db) + .is_some_and(|tuple_class| !special_form.is_instance_of(db, tuple_class)), + + (Type::KnownInstance(known_instance), Type::Tuple(tuple)) + | (Type::Tuple(tuple), Type::KnownInstance(known_instance)) => tuple + .to_class_type(db) + .is_some_and(|tuple_class| !known_instance.is_instance_of(db, tuple_class)), (Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => { @@ -2113,18 +2101,14 @@ impl<'db> Type<'db> { } (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { - let self_elements = tuple.elements(db); - let other_elements = other_tuple.elements(db); - self_elements.len() != other_elements.len() - || self_elements - .iter() - .zip(other_elements) - .any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) + tuple.is_disjoint_from(db, other_tuple) } - (Type::Tuple(tuple), instance @ Type::NominalInstance(_)) - | (instance @ Type::NominalInstance(_), Type::Tuple(tuple)) => { - instance.is_disjoint_from(db, tuple.homogeneous_supertype(db)) + (Type::Tuple(tuple), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::Tuple(tuple)) => { + tuple.to_class_type(db).is_some_and(|tuple_class| { + instance.is_disjoint_from_nominal_instance_of_class(db, tuple_class) + }) } (Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => { @@ -2203,10 +2187,7 @@ impl<'db> Type<'db> { // containing gradual forms such as `tuple[Any, ...]`. // Conversely, make sure to return `true` for homogeneous tuples such as // `tuple[int, ...]`, once we add support for them. - Type::Tuple(tuple) => tuple - .elements(db) - .iter() - .all(|elem| elem.is_fully_static(db)), + Type::Tuple(tuple) => tuple.is_fully_static(db), Type::Callable(callable) => callable.is_fully_static(db), Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db), } @@ -2379,11 +2360,7 @@ impl<'db> Type<'db> { false } - Type::Tuple(tuple) => tuple - .elements(db) - .iter() - .all(|elem| elem.is_single_valued(db)), - + Type::Tuple(tuple) => tuple.is_single_valued(db), Type::NominalInstance(instance) => instance.is_single_valued(db), Type::BoundSuper(_) => { @@ -2629,7 +2606,10 @@ impl<'db> Type<'db> { KnownClass::Str.to_instance(db).instance_member(db, name) } Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name), - Type::Tuple(tuple) => tuple.homogeneous_supertype(db).instance_member(db, name), + Type::Tuple(tuple) => tuple + .to_class_type(db) + .map(|class| class.instance_member(db, name)) + .unwrap_or(Place::Unbound.into()), Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name), Type::ModuleLiteral(_) => KnownClass::ModuleType @@ -3474,7 +3454,7 @@ impl<'db> Type<'db> { Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), - Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()), + Type::Tuple(tuple) => Truthiness::from(!tuple.tuple(db).is_empty()), }; Ok(truthiness) @@ -3505,7 +3485,11 @@ impl<'db> Type<'db> { let usize_len = match self { Type::BytesLiteral(bytes) => Some(bytes.python_len(db)), Type::StringLiteral(string) => Some(string.python_len(db)), - Type::Tuple(tuple) => Some(tuple.len(db)), + Type::Tuple(tuple) => match tuple.tuple(db) { + TupleSpec::Fixed(tuple) => Some(tuple.len()), + TupleSpec::Variable(_) => None, + }, + _ => None, }; @@ -3705,10 +3689,7 @@ impl<'db> Type<'db> { db, [ KnownClass::Str.to_instance(db), - KnownClass::Tuple.to_specialized_instance( - db, - [KnownClass::Str.to_instance(db)], - ), + TupleType::homogeneous(db, KnownClass::Str.to_instance(db)), ], )), Parameter::positional_only(Some(Name::new_static("start"))) @@ -4022,10 +4003,10 @@ impl<'db> Type<'db> { Parameter::positional_only(Some(Name::new_static("name"))) .with_annotated_type(str_instance), Parameter::positional_only(Some(Name::new_static("bases"))) - .with_annotated_type( - KnownClass::Tuple - .to_specialized_instance(db, [type_instance]), - ), + .with_annotated_type(TupleType::homogeneous( + db, + type_instance, + )), Parameter::positional_only(Some(Name::new_static("dict"))) .with_annotated_type( KnownClass::Dict.to_specialized_instance( @@ -4173,16 +4154,16 @@ impl<'db> Type<'db> { .with_annotated_type(Type::any()) .type_form(), Parameter::keyword_only(Name::new_static("type_params")) - .with_annotated_type(KnownClass::Tuple.to_specialized_instance( + .with_annotated_type(TupleType::homogeneous( db, - [UnionType::from_elements( + UnionType::from_elements( db, [ KnownClass::TypeVar.to_instance(db), KnownClass::ParamSpec.to_instance(db), KnownClass::TypeVarTuple.to_instance(db), ], - )], + ), )) .with_default_type(TupleType::empty(db)), ]), @@ -4476,7 +4457,10 @@ impl<'db> Type<'db> { /// ``` fn try_iterate(self, db: &'db dyn Db) -> Result, IterationError<'db>> { if let Type::Tuple(tuple_type) = self { - return Ok(UnionType::from_elements(db, tuple_type.elements(db))); + return Ok(UnionType::from_elements( + db, + tuple_type.tuple(db).all_elements(), + )); } if let Type::GenericAlias(alias) = self { @@ -4639,20 +4623,20 @@ impl<'db> Type<'db> { // have the class's typevars still in the method signature when we attempt to call it. To // do this, we instead use the _identity_ specialization, which maps each of the class's // generic typevars to itself. - let (generic_origin, generic_context, self_type) = match self { - Type::ClassLiteral(class) => match class.generic_context(db) { - Some(generic_context) => { - let specialization = generic_context.identity_specialization(db); - ( + let (generic_origin, generic_context, self_type) = + match self { + Type::ClassLiteral(class) => match class.generic_context(db) { + Some(generic_context) => ( Some(class), Some(generic_context), - Type::GenericAlias(GenericAlias::new(db, class, specialization)), - ) - } + Type::from(class.apply_specialization(db, |_| { + generic_context.identity_specialization(db) + })), + ), + _ => (None, None, self), + }, _ => (None, None, self), - }, - _ => (None, None, self), - }; + }; // As of now we do not model custom `__call__` on meta-classes, so the code below // only deals with interplay between `__new__` and `__init__` methods. @@ -4775,11 +4759,7 @@ impl<'db> Type<'db> { .map(|specialization| { Type::instance( db, - ClassType::Generic(GenericAlias::new( - db, - generic_origin, - specialization, - )), + generic_origin.apply_specialization(db, |_| specialization), ) }) .unwrap_or(instance_ty); @@ -4966,7 +4946,7 @@ impl<'db> Type<'db> { // We treat `typing.Type` exactly the same as `builtins.type`: SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), - SpecialFormType::Tuple => Ok(KnownClass::Tuple.to_instance(db)), + SpecialFormType::Tuple => Ok(TupleType::homogeneous(db, Type::unknown())), // Legacy `typing` aliases SpecialFormType::List => Ok(KnownClass::List.to_instance(db)), @@ -5189,7 +5169,10 @@ impl<'db> Type<'db> { } Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), - Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), + Type::Tuple(tuple) => tuple + .to_class_type(db) + .map(Type::from) + .unwrap_or_else(Type::unknown), Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => KnownClass::Type.to_instance(db), @@ -5343,12 +5326,7 @@ impl<'db> Type<'db> { } builder.build() } - Type::Tuple(tuple) => TupleType::from_elements( - db, - tuple - .iter(db) - .map(|ty| ty.apply_type_mapping(db, type_mapping)), - ), + Type::Tuple(tuple) => Type::Tuple(tuple.apply_type_mapping(db, type_mapping)), Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)), @@ -5439,10 +5417,9 @@ impl<'db> Type<'db> { negative.find_legacy_typevars(db, typevars); } } + Type::Tuple(tuple) => { - for element in tuple.iter(db) { - element.find_legacy_typevars(db, typevars); - } + tuple.find_legacy_typevars(db, typevars); } Type::GenericAlias(alias) => { @@ -8134,89 +8111,6 @@ impl<'db> BytesLiteralType<'db> { } } -/// # Ordering -/// Ordering is based on the tuple's salsa-assigned id and not on its elements. -/// The id may change between runs, or when the tuple was garbage collected and recreated. -#[salsa::interned(debug)] -#[derive(PartialOrd, Ord)] -pub struct TupleType<'db> { - #[returns(deref)] - elements: Box<[Type<'db>]>, -} - -impl<'db> TupleType<'db> { - fn homogeneous_supertype(self, db: &'db dyn Db) -> Type<'db> { - KnownClass::Tuple - .to_specialized_instance(db, [UnionType::from_elements(db, self.elements(db))]) - } - - pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> { - Type::Tuple(TupleType::new(db, Box::<[Type<'db>]>::from([]))) - } - - pub(crate) fn from_elements>>( - db: &'db dyn Db, - types: impl IntoIterator, - ) -> Type<'db> { - let mut elements = vec![]; - - for ty in types { - let ty = ty.into(); - if ty.is_never() { - return Type::Never; - } - elements.push(ty); - } - - Type::Tuple(Self::new(db, elements.into_boxed_slice())) - } - - /// Return a normalized version of `self`. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - let elements: Box<[Type<'db>]> = self - .elements(db) - .iter() - .map(|ty| ty.normalized(db)) - .collect(); - TupleType::new(db, elements) - } - - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - let self_elements = self.elements(db); - let other_elements = other.elements(db); - self_elements.len() == other_elements.len() - && self_elements - .iter() - .zip(other_elements) - .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) - } - - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - let self_elements = self.elements(db); - let other_elements = other.elements(db); - self_elements.len() == other_elements.len() - && self_elements - .iter() - .zip(other_elements) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } - - pub fn get(&self, db: &'db dyn Db, index: usize) -> Option> { - self.elements(db).get(index).copied() - } - - pub fn len(&self, db: &'db dyn Db) -> usize { - self.elements(db).len() - } - - pub fn iter(&self, db: &'db dyn Db) -> impl Iterator> + 'db + '_ { - self.elements(db).iter().copied() - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum BoundSuperError<'db> { InvalidPivotClassType { diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 3ed581e415180..8b749f1f4281d 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -4,7 +4,8 @@ use std::ops::{Deref, DerefMut}; use itertools::{Either, Itertools}; use crate::Db; -use crate::types::{KnownClass, TupleType}; +use crate::types::KnownClass; +use crate::types::tuple::{TupleSpec, TupleType}; use super::Type; @@ -210,11 +211,15 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { Type::BooleanLiteral(false), ]) } - Type::Tuple(tuple) => { + Type::Tuple(tuple_type) => { // Note: This should only account for tuples of known length, i.e., `tuple[bool, ...]` // should not be expanded here. + let tuple = tuple_type.tuple(db); + if !matches!(tuple, TupleSpec::Fixed(_)) { + return None; + } let expanded = tuple - .iter(db) + .all_elements() .map(|element| { if let Some(expanded) = expand_type(db, element) { Either::Left(expanded.into_iter()) @@ -242,7 +247,8 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { #[cfg(test)] mod tests { use crate::db::tests::setup_db; - use crate::types::{KnownClass, TupleType, Type, UnionType}; + use crate::types::tuple::TupleType; + use crate::types::{KnownClass, Type, UnionType}; use super::expand_type; @@ -308,7 +314,6 @@ mod tests { TupleType::from_elements(&db, [false_ty, bytes_ty]), ]; let expanded = expand_type(&db, tuple_type2).unwrap(); - assert_eq!(expanded.len(), expected_types.len()); assert_eq!(expanded, expected_types); // Mixed set of elements where some can be expanded while others cannot be. @@ -328,7 +333,16 @@ mod tests { TupleType::from_elements(&db, [false_ty, int_ty, bytes_ty, str_ty]), ]; let expanded = expand_type(&db, tuple_type3).unwrap(); - assert_eq!(expanded.len(), expected_types.len()); assert_eq!(expanded, expected_types); + + // Variable-length tuples are not expanded. + let variable_length_tuple = TupleType::mixed( + &db, + [bool_ty], + int_ty, + [UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty], + ); + let expanded = expand_type(&db, variable_length_tuple); + assert!(expanded.is_none()); } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index f24afbf7242d2..16f9686688ef7 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -27,9 +27,10 @@ use crate::types::function::{ }; use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError}; use crate::types::signatures::{Parameter, ParameterForm}; +use crate::types::tuple::TupleType; use crate::types::{ BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType, - MethodWrapperKind, PropertyInstanceType, SpecialFormType, TupleType, TypeMapping, UnionType, + MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, WrapperDescriptorKind, ide_support, todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 208091f0e6300..67ee634197904 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -12,6 +12,7 @@ use crate::semantic_index::definition::{Definition, DefinitionState}; use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; +use crate::types::tuple::TupleType; use crate::types::{ CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance, }; @@ -30,8 +31,8 @@ use crate::{ place_table, semantic_index, use_def_map, }, types::{ - CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder, - UnionType, definition_expression_type, + CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType, + definition_expression_type, }, }; use indexmap::IndexSet; @@ -203,6 +204,8 @@ impl<'db> GenericAlias<'db> { db: &'db dyn Db, typevars: &mut FxOrderSet>, ) { + // A tuple's specialization will include all of its element types, so we don't need to also + // look in `self.tuple`. self.specialization(db).find_legacy_typevars(db, typevars); } } @@ -761,58 +764,46 @@ impl<'db> ClassLiteral<'db> { index.expect_single_definition(body_scope.node(db).expect_class(&module)) } - pub(crate) fn apply_optional_specialization( + pub(crate) fn apply_specialization( self, db: &'db dyn Db, - specialization: Option>, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, ) -> ClassType<'db> { - match (self.generic_context(db), specialization) { - (None, _) => ClassType::NonGeneric(self), - (Some(generic_context), None) => { - let specialization = generic_context.default_specialization(db); - ClassType::Generic(GenericAlias::new(db, self, specialization)) - } - (Some(_), Some(specialization)) => { + match self.generic_context(db) { + None => ClassType::NonGeneric(self), + Some(generic_context) => { + let specialization = f(generic_context); ClassType::Generic(GenericAlias::new(db, self, specialization)) } } } + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + specialization.unwrap_or_else(|| generic_context.default_specialization(db)) + }) + } + /// Returns the default specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self.generic_context(db) { - None => ClassType::NonGeneric(self), - Some(generic_context) => { - let specialization = generic_context.default_specialization(db); - ClassType::Generic(GenericAlias::new(db, self, specialization)) - } - } - } - - /// Returns a specialization of this class with a `@Todo`-type - pub(crate) fn todo_specialization(self, db: &'db dyn Db, todo: &'static str) -> ClassType<'db> { - match self.generic_context(db) { - None => ClassType::NonGeneric(self), - Some(generic_context) => { - let specialization = generic_context.todo_specialization(db, todo); - ClassType::Generic(GenericAlias::new(db, self, specialization)) - } - } + self.apply_specialization(db, |generic_context| { + generic_context.default_specialization(db) + }) } /// Returns the unknown specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self.generic_context(db) { - None => ClassType::NonGeneric(self), - Some(generic_context) => { - let specialization = generic_context.unknown_specialization(db); - ClassType::Generic(GenericAlias::new(db, self, specialization)) - } - } + self.apply_specialization(db, |generic_context| { + generic_context.unknown_specialization(db) + }) } /// Return an iterator over the inferred types of this class's *explicit* bases. @@ -2448,22 +2439,20 @@ impl<'db> KnownClass { .unwrap_or_else(Type::unknown) } - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing all possible instances of the generic class with a specialization. + /// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`] + /// representing a specialization of that class. /// /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong /// number of types, a debug-level log message will be emitted stating this. - pub(crate) fn to_specialized_instance( + pub(crate) fn to_specialized_class_type( self, db: &'db dyn Db, specialization: impl IntoIterator>, - ) -> Type<'db> { + ) -> Option> { let Type::ClassLiteral(class_literal) = self.to_class_literal(db) else { - return Type::unknown(); - }; - let Some(generic_context) = class_literal.generic_context(db) else { - return Type::instance(db, ClassType::NonGeneric(class_literal)); + return None; }; + let generic_context = class_literal.generic_context(db)?; let types = specialization.into_iter().collect::>(); if types.len() != generic_context.len(db) { @@ -2477,21 +2466,32 @@ impl<'db> KnownClass { self.display(db) ); } - return Type::instance(db, class_literal.default_specialization(db)); + return Some(class_literal.default_specialization(db)); } - let specialization = generic_context.specialize(db, types); - Type::instance( - db, - ClassType::Generic(GenericAlias::new(db, class_literal, specialization)), - ) + Some(class_literal.apply_specialization(db, |_| generic_context.specialize(db, types))) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing all possible instances of the generic class with a specialization. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + pub(crate) fn to_specialized_instance( + self, + db: &'db dyn Db, + specialization: impl IntoIterator>, + ) -> Type<'db> { + self.to_specialized_class_type(db, specialization) + .and_then(|class_type| Type::from(class_type).to_instance(db)) + .unwrap_or_else(Type::unknown) } /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. /// /// Return an error if the symbol cannot be found in the expected typeshed module, /// or if the symbol is not a class definition, or if the symbol is possibly unbound. - pub(crate) fn try_to_class_literal( + fn try_to_class_literal_without_logging( self, db: &'db dyn Db, ) -> Result, KnownClassLookupError<'db>> { @@ -2511,14 +2511,13 @@ impl<'db> KnownClass { /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. /// /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn try_to_class_literal(self, db: &'db dyn Db) -> Option> { // a cache of the `KnownClass`es that we have already failed to lookup in typeshed // (and therefore that we've already logged a warning for) static MESSAGES: LazyLock>> = LazyLock::new(Mutex::default); - self.try_to_class_literal(db) - .map(Type::ClassLiteral) - .unwrap_or_else(|lookup_error| { + self.try_to_class_literal_without_logging(db) + .or_else(|lookup_error| { if MESSAGES.lock().unwrap().insert(self) { if matches!( lookup_error, @@ -2535,12 +2534,22 @@ impl<'db> KnownClass { match lookup_error { KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { - class_literal.into() + Ok(class_literal) } KnownClassLookupError::ClassNotFound { .. } - | KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(), + | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), } }) + .ok() + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> { + self.try_to_class_literal(db) + .map(Type::ClassLiteral) + .unwrap_or_else(Type::unknown) } /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] @@ -2557,7 +2566,7 @@ impl<'db> KnownClass { /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, /// *and* `class` is a subclass of `other`. pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.try_to_class_literal(db) + self.try_to_class_literal_without_logging(db) .is_ok_and(|class| class.is_subclass_of(db, None, other)) } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index c9c51647c9f58..8a9d6856938ce 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -68,6 +68,7 @@ impl<'db> ClassBase<'db> { if literal.is_known(db, KnownClass::Any) { Some(Self::Dynamic(DynamicType::Any)) } else if literal.is_known(db, KnownClass::NamedTuple) { + // TODO: Figure out the tuple spec for the named tuple Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db)) } else { Some(Self::Class(literal.default_specialization(db))) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index f39adacf63b19..fa71871e8b83b 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -14,6 +14,7 @@ use crate::types::string_annotation::{ IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, }; +use crate::types::tuple::TupleType; use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral}; use crate::{Db, Module, ModuleName, Program, declare_lint}; use itertools::Itertools; @@ -1606,7 +1607,7 @@ pub(super) fn report_index_out_of_bounds( kind: &'static str, node: AnyNodeRef, tuple_ty: Type, - length: usize, + length: impl std::fmt::Display, index: i64, ) { let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else { @@ -2120,7 +2121,7 @@ pub(crate) fn report_invalid_or_unsupported_base( return; } - let tuple_of_types = KnownClass::Tuple.to_specialized_instance(db, [instance_of_type]); + let tuple_of_types = TupleType::homogeneous(db, instance_of_type); let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| { diagnostic.info( diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index fd2ad5bea6b47..cab663e29e6bb 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -10,6 +10,7 @@ use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::{FunctionType, OverloadLiteral}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; +use crate::types::tuple::TupleSpec; use crate::types::{ CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, @@ -190,16 +191,7 @@ impl Display for DisplayRepresentation<'_> { escape.bytes_repr(TripleQuotes::No).write(f) } - Type::Tuple(tuple) => { - f.write_str("tuple[")?; - let elements = tuple.elements(self.db); - if elements.is_empty() { - f.write_str("()")?; - } else { - elements.display(self.db).fmt(f)?; - } - f.write_str("]") - } + Type::Tuple(specialization) => specialization.tuple(self.db).display(self.db).fmt(f), Type::TypeVar(typevar) => f.write_str(typevar.name(self.db)), Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), @@ -224,6 +216,67 @@ impl Display for DisplayRepresentation<'_> { } } +impl<'db> TupleSpec<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayTuple<'db> { + DisplayTuple { tuple: self, db } + } +} + +pub(crate) struct DisplayTuple<'db> { + tuple: &'db TupleSpec<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayTuple<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("tuple[")?; + match self.tuple { + TupleSpec::Fixed(tuple) => { + let elements = tuple.elements_slice(); + if elements.is_empty() { + f.write_str("()")?; + } else { + elements.display(self.db).fmt(f)?; + } + } + + // Decoder key for which snippets of text need to be included depending on whether + // the tuple contains a prefix and/or suffix: + // + // tuple[ yyy, ... ] + // tuple[xxx, *tuple[yyy, ...] ] + // tuple[xxx, *tuple[yyy, ...], zzz] + // tuple[ *tuple[yyy, ...], zzz] + // PPPPPPPPPPPP P + // SSSSSSS SSSSSS + // + // (Anything that appears above only a P is included only if there's a prefix; anything + // above only an S is included only if there's a suffix; anything about both a P and an + // S is included if there is either a prefix or a suffix. The initial `tuple[` and + // trailing `]` are printed elsewhere. The `yyy, ...` is printed no matter what.) + TupleSpec::Variable(tuple) => { + if !tuple.prefix.is_empty() { + tuple.prefix.display(self.db).fmt(f)?; + f.write_str(", ")?; + } + if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { + f.write_str("*tuple[")?; + } + tuple.variable.display(self.db).fmt(f)?; + f.write_str(", ...")?; + if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { + f.write_str("]")?; + } + if !tuple.suffix.is_empty() { + f.write_str(", ")?; + tuple.suffix.display(self.db).fmt(f)?; + } + } + } + f.write_str("]") + } +} + impl<'db> OverloadLiteral<'db> { // Not currently used, but useful for debugging. #[expect(dead_code)] @@ -307,15 +360,19 @@ pub(crate) struct DisplayGenericAlias<'db> { impl Display for DisplayGenericAlias<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "{origin}{specialization}", - origin = self.origin.name(self.db), - specialization = self.specialization.display_short( - self.db, - TupleSpecialization::from_class(self.db, self.origin) - ), - ) + if self.origin.is_known(self.db, KnownClass::Tuple) { + self.specialization.tuple(self.db).display(self.db).fmt(f) + } else { + write!( + f, + "{origin}{specialization}", + origin = self.origin.name(self.db), + specialization = self.specialization.display_short( + self.db, + TupleSpecialization::from_class(self.db, self.origin) + ), + ) + } } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 56c4df34262e6..bec2a2579e9ae 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -8,9 +8,10 @@ use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType}; use crate::types::signatures::{Parameter, Parameters, Signature}; +use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::{ KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, - TypeVarVariance, UnionType, declaration_type, todo_type, + TypeVarVariance, UnionType, declaration_type, }; use crate::{Db, FxOrderSet}; @@ -143,20 +144,6 @@ impl<'db> GenericContext<'db> { self.specialize_partial(db, &vec![None; self.variables(db).len()]) } - #[allow(unused_variables)] // Only unused in release builds - pub(crate) fn todo_specialization( - self, - db: &'db dyn Db, - todo: &'static str, - ) -> Specialization<'db> { - let types = self - .variables(db) - .iter() - .map(|typevar| typevar.default_ty(db).unwrap_or(todo_type!(todo))) - .collect(); - self.specialize(db, types) - } - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { let types = self .variables(db) @@ -185,7 +172,17 @@ impl<'db> GenericContext<'db> { types: Box<[Type<'db>]>, ) -> Specialization<'db> { assert!(self.variables(db).len() == types.len()); - Specialization::new(db, self, types) + Specialization::new(db, self, types, None) + } + + /// Creates a specialization of this generic context for the `tuple` class. + pub(crate) fn specialize_tuple( + self, + db: &'db dyn Db, + tuple: TupleType<'db>, + ) -> Specialization<'db> { + let element_type = UnionType::from_elements(db, tuple.tuple(db).all_elements()); + Specialization::new(db, self, Box::from([element_type]), Some(tuple)) } /// Creates a specialization of this generic context. Panics if the length of `types` does not @@ -230,7 +227,7 @@ impl<'db> GenericContext<'db> { expanded[idx] = default; } - Specialization::new(db, self, expanded.into_boxed_slice()) + Specialization::new(db, self, expanded.into_boxed_slice(), None) } pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { @@ -273,9 +270,24 @@ pub struct Specialization<'db> { pub(crate) generic_context: GenericContext<'db>, #[returns(deref)] pub(crate) types: Box<[Type<'db>]>, + + /// For specializations of `tuple`, we also store more detailed information about the tuple's + /// elements, above what the class's (single) typevar can represent. + tuple_inner: Option>, } impl<'db> Specialization<'db> { + /// Returns the tuple spec for a specialization of the `tuple` class. + pub(crate) fn tuple(self, db: &'db dyn Db) -> &'db TupleSpec<'db> { + if let Some(tuple) = self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) { + return tuple; + } + if let [element_type] = self.types(db) { + return TupleType::new(db, TupleSpec::homogeneous(*element_type)).tuple(db); + } + TupleType::new(db, TupleSpec::homogeneous(Type::unknown())).tuple(db) + } + /// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this /// mapping. pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option> { @@ -313,7 +325,10 @@ impl<'db> Specialization<'db> { .iter() .map(|ty| ty.apply_type_mapping(db, type_mapping)) .collect(); - Specialization::new(db, self.generic_context(db), types) + let tuple_inner = self + .tuple_inner(db) + .map(|tuple| tuple.apply_type_mapping(db, type_mapping)); + Specialization::new(db, self.generic_context(db), types, tuple_inner) } /// Applies an optional specialization to this specialization. @@ -350,12 +365,14 @@ impl<'db> Specialization<'db> { _ => UnionType::from_elements(db, [self_type, other_type]), }) .collect(); - Specialization::new(db, self.generic_context(db), types) + // TODO: Combine the tuple specs too + Specialization::new(db, self.generic_context(db), types, None) } pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect(); - Self::new(db, self.generic_context(db), types) + let tuple_inner = self.tuple_inner(db).map(|tuple| tuple.normalized(db)); + Self::new(db, self.generic_context(db), types, tuple_inner) } pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { @@ -374,7 +391,11 @@ impl<'db> Specialization<'db> { vartype.materialize(db, variance) }) .collect(); - Specialization::new(db, self.generic_context(db), types) + let tuple_inner = self.tuple_inner(db).map(|tuple| { + // Tuples are immutable, so tuple element types are always in covariant position. + tuple.materialize(db, variance) + }); + Specialization::new(db, self.generic_context(db), types, tuple_inner) } pub(crate) fn has_relation_to( @@ -388,6 +409,11 @@ impl<'db> Specialization<'db> { return false; } + if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) + { + return self_tuple.has_relation_to(db, other_tuple, relation); + } + for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) .zip(self.types(db)) .zip(other.types(db)) @@ -570,7 +596,8 @@ impl<'db> SpecializationBuilder<'db> { .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) }) .collect(); - Specialization::new(self.db, generic_context, types) + // TODO Infer the tuple spec for a tuple type + Specialization::new(self.db, generic_context, types, None) } fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) { @@ -641,14 +668,19 @@ impl<'db> SpecializationBuilder<'db> { } (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { - let formal_elements = formal_tuple.elements(self.db); - let actual_elements = actual_tuple.elements(self.db); - if formal_elements.len() == actual_elements.len() { - for (formal_element, actual_element) in - formal_elements.iter().zip(actual_elements) - { - self.infer(*formal_element, *actual_element)?; + let formal_tuple = formal_tuple.tuple(self.db); + let actual_tuple = actual_tuple.tuple(self.db); + match (formal_tuple, actual_tuple) { + (TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => { + if formal_tuple.len() == actual_tuple.len() { + for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) { + self.infer(formal_element, actual_element)?; + } + } } + + // TODO: Infer specializations of variable-length tuples + (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) => {} } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 1e491e478edbf..0dac4db19b969 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -117,11 +117,16 @@ impl AllMembers { | Type::KnownInstance(_) | Type::TypeVar(_) | Type::BoundSuper(_) - | Type::TypeIs(_) => { - if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { + | Type::TypeIs(_) => match ty.to_meta_type(db) { + Type::ClassLiteral(class_literal) => { self.extend_with_class_members(db, class_literal); } - } + Type::GenericAlias(generic_alias) => { + let class_literal = generic_alias.origin(db); + self.extend_with_class_members(db, class_literal); + } + _ => {} + }, Type::ModuleLiteral(literal) => { self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index af720ca38c8ca..e11a31ab424b9 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -95,15 +95,16 @@ use crate::types::function::{ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::signatures::{CallableSignature, Signature}; +use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, - DynamicType, GenericAlias, IntersectionBuilder, IntersectionType, KnownClass, - KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, - PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, StringLiteralType, - SubclassOfType, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, - TypeArrayDisplay, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type, + DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, + LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, + ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness, + Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers, + TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, + UnionType, binding_type, todo_type, }; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; @@ -2443,7 +2444,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo_type!("PEP 646") } else { let annotated_type = self.file_expression_type(annotation); - KnownClass::Tuple.to_specialized_instance(self.db(), [annotated_type]) + TupleType::homogeneous(self.db(), annotated_type) }; self.add_declaration_with_binding( @@ -2455,7 +2456,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.add_binding( parameter.into(), definition, - KnownClass::Tuple.to_specialized_instance(self.db(), [Type::unknown()]), + TupleType::homogeneous(self.db(), Type::unknown()), ); } } @@ -2832,7 +2833,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. let symbol_ty = if let Type::Tuple(tuple) = node_ty { let mut builder = UnionBuilder::new(self.db()); - for element in tuple.elements(self.db()).iter().copied() { + for element in tuple.tuple(self.db()).all_elements() { builder = builder.add( if element.is_assignable_to(self.db(), type_base_exception) { element.to_instance(self.db()).expect( @@ -2855,7 +2856,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } else if node_ty.is_assignable_to( self.db(), - KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]), + TupleType::homogeneous(self.db(), type_base_exception), ) { extract_tuple_specialization(self.db(), node_ty) .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())) @@ -2865,7 +2866,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.db(), [ type_base_exception, - KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]), + TupleType::homogeneous(self.db(), type_base_exception), ], ), ) { @@ -3698,9 +3699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::List(ast::ExprList { elts, .. }) | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { let mut assigned_tys = match assigned_ty { - Some(Type::Tuple(tuple)) => { - Either::Left(tuple.elements(self.db()).iter().copied()) - } + Some(Type::Tuple(tuple)) => Either::Left(tuple.tuple(self.db()).all_elements()), Some(_) | None => Either::Right(std::iter::empty()), }; @@ -6940,6 +6939,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitXor) => { Some(Type::BooleanLiteral(b1 ^ b2)) } + (Type::BooleanLiteral(b1), Type::BooleanLiteral(_) | Type::IntLiteral(_), op) => self .infer_binary_expression_type( node, @@ -6956,19 +6956,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::IntLiteral(i64::from(b2)), op, ), - (Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => { - // Note: this only works on heterogeneous tuples. - let lhs_elements = lhs.elements(self.db()); - let rhs_elements = rhs.elements(self.db()); - Some(TupleType::from_elements( + (Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => Some(Type::tuple( + self.db(), + TupleType::new( self.db(), - lhs_elements - .iter() - .copied() - .chain(rhs_elements.iter().copied()), - )) - } + lhs.tuple(self.db()).concat(self.db(), rhs.tuple(self.db())), + ), + )), // We've handled all of the special cases that we support for literals, so we need to // fall back on looking for dunder methods on one of the operand types. @@ -7425,19 +7420,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // tuples. // // Ref: https://github.com/astral-sh/ruff/pull/18251#discussion_r2115909311 - if tuple.len(self.db()) > 1 << 12 { + let (minimum_length, _) = tuple.tuple(self.db()).size_hint(); + if minimum_length > 1 << 12 { return None; } let mut definitely_true = false; let mut definitely_false = true; - for element in tuple.elements(self.db()) { + for element in tuple.tuple(self.db()).all_elements() { if element.is_string_literal() { - if literal == *element { + if literal == element { definitely_true = true; definitely_false = false; } - } else if !literal.is_disjoint_from(self.db(), *element) { + } else if !literal.is_disjoint_from(self.db(), element) { definitely_false = false; } } @@ -7697,12 +7693,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } (Type::Tuple(lhs), Type::Tuple(rhs)) => { - // Note: This only works on heterogeneous tuple types. - let lhs_elements = lhs.elements(self.db()); - let rhs_elements = rhs.elements(self.db()); + let lhs_tuple = lhs.tuple(self.db()); + let rhs_tuple = rhs.tuple(self.db()); let mut tuple_rich_comparison = - |op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements, range); + |op| self.infer_tuple_rich_comparison(lhs_tuple, op, rhs_tuple, range); match op { ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), @@ -7712,14 +7707,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), ast::CmpOp::In | ast::CmpOp::NotIn => { - let mut eq_count = 0usize; - let mut not_eq_count = 0usize; + let mut any_eq = false; + let mut any_ambiguous = false; - for ty in rhs_elements { + for ty in rhs_tuple.all_elements() { let eq_result = self.infer_binary_type_comparison( Type::Tuple(lhs), ast::CmpOp::Eq, - *ty, + ty, range, ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); @@ -7729,16 +7724,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // for different union variants. Instead, this is just for us to // evaluate a possibly truthy value to `false` or `true`. ty => match ty.bool(self.db()) { - Truthiness::AlwaysTrue => eq_count += 1, - Truthiness::AlwaysFalse => not_eq_count += 1, - Truthiness::Ambiguous => (), + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, }, } } - if eq_count >= 1 { + if any_eq { Ok(Type::BooleanLiteral(op.is_in())) - } else if not_eq_count == rhs_elements.len() { + } else if !any_ambiguous { Ok(Type::BooleanLiteral(op.is_not_in())) } else { Ok(KnownClass::Bool.to_instance(self.db())) @@ -7914,13 +7909,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// see `` fn infer_tuple_rich_comparison( &mut self, - left: &[Type<'db>], + left: &TupleSpec<'db>, op: RichCompareOperator, - right: &[Type<'db>], + right: &TupleSpec<'db>, range: TextRange, ) -> Result, CompareUnsupportedError<'db>> { - let left_iter = left.iter().copied(); - let right_iter = right.iter().copied(); + // If either tuple is variable length, we can make no assumptions about the relative + // lengths of the tuples, and therefore neither about how they compare lexicographically. + // TODO: Consider comparing the prefixes of the tuples, since that could give a comparison + // result regardless of how long the variable-length tuple is. + let (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) = (left, right) else { + return Ok(Type::unknown()); + }; + + let left_iter = left.elements(); + let right_iter = right.elements(); let mut builder = UnionBuilder::new(self.db()); @@ -8052,11 +8055,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // special cases, too. if let Type::ClassLiteral(class) = value_ty { if class.is_known(self.db(), KnownClass::Tuple) { - self.infer_expression(slice); - // TODO heterogeneous and homogeneous tuples in value expressions - return Type::from( - class.todo_specialization(self.db(), "Generic tuple specializations"), - ); + return self + .infer_tuple_type_expression(slice) + .to_meta_type(self.db()); } if let Some(generic_context) = class.generic_context(self.db()) { return self.infer_explicit_class_specialization( @@ -8067,6 +8068,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } + if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { + return self + .infer_tuple_type_expression(slice) + .to_meta_type(self.db()); + } let slice_ty = self.infer_expression(slice); let result_ty = self.infer_subscript_expression_types(value, value_ty, slice_ty); @@ -8113,9 +8119,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .matching_overloads() .next() .expect("valid bindings should have matching overload"); - let specialization = - generic_context.specialize_partial(self.db(), overload.parameter_types()); - Type::from(GenericAlias::new(self.db(), generic_class, specialization)) + Type::from(generic_class.apply_specialization(self.db(), |_| { + generic_context.specialize_partial(self.db(), overload.parameter_types()) + })) } fn infer_subscript_expression_types( @@ -8137,18 +8143,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::Tuple(tuple_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => { - let elements = tuple_ty.elements(self.db()); - elements - .iter() - .py_index(i32::try_from(int).expect("checked in branch arm")) - .copied() + let tuple = tuple_ty.tuple(self.db()); + tuple + .py_index( + self.db(), + i32::try_from(int).expect("checked in branch arm"), + ) .unwrap_or_else(|_| { report_index_out_of_bounds( &self.context, "tuple", value_node.into(), value_ty, - elements.len(), + tuple.display_minimum_length(), int, ); Type::unknown() @@ -8156,9 +8163,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` (Type::Tuple(tuple_ty), _, Some(SliceLiteral { start, stop, step })) => { - let elements = tuple_ty.elements(self.db()); + let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else { + return todo_type!("slice into variable-length tuple"); + }; - if let Ok(new_elements) = elements.py_slice(start, stop, step) { + if let Ok(new_elements) = tuple.py_slice(self.db(), start, stop, step) { TupleType::from_elements(self.db(), new_elements) } else { report_slice_step_size_zero(&self.context, value_node.into()); @@ -8170,9 +8179,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if i32::try_from(int).is_ok() => { let literal_value = literal_ty.value(self.db()); - literal_value - .chars() - .py_index(i32::try_from(int).expect("checked in branch arm")) + (&mut literal_value.chars()) + .py_index( + self.db(), + i32::try_from(int).expect("checked in branch arm"), + ) .map(|ch| Type::string_literal(self.db(), &ch.to_string())) .unwrap_or_else(|_| { report_index_out_of_bounds( @@ -8192,7 +8203,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let chars: Vec<_> = literal_value.chars().collect(); - if let Ok(new_chars) = chars.py_slice(start, stop, step) { + if let Ok(new_chars) = chars.py_slice(self.db(), start, stop, step) { let literal: String = new_chars.collect(); Type::string_literal(self.db(), &literal) } else { @@ -8206,8 +8217,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let literal_value = literal_ty.value(self.db()); literal_value - .iter() - .py_index(i32::try_from(int).expect("checked in branch arm")) + .py_index( + self.db(), + i32::try_from(int).expect("checked in branch arm"), + ) .map(|byte| Type::IntLiteral((*byte).into())) .unwrap_or_else(|_| { report_index_out_of_bounds( @@ -8225,7 +8238,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (Type::BytesLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => { let literal_value = literal_ty.value(self.db()); - if let Ok(new_bytes) = literal_value.py_slice(start, stop, step) { + if let Ok(new_bytes) = literal_value.py_slice(self.db(), start, stop, step) { let new_bytes: Vec = new_bytes.copied().collect(); Type::bytes_literal(self.db(), &new_bytes) } else { @@ -8243,14 +8256,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty, Type::IntLiteral(i64::from(bool)), ), - (Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => self - .legacy_generic_class_context( + (Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => { + let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else { + // TODO: emit a diagnostic + return Type::unknown(); + }; + self.legacy_generic_class_context( value_node, - typevars.elements(self.db()), + typevars.elements_slice(), LegacyGenericBase::Protocol, ) .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))) - .unwrap_or_else(Type::unknown), + .unwrap_or_else(Type::unknown) + } (Type::SpecialForm(SpecialFormType::Protocol), typevar, _) => self .legacy_generic_class_context( value_node, @@ -8263,14 +8281,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // TODO: emit a diagnostic todo_type!("doubly-specialized typing.Protocol") } - (Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => self - .legacy_generic_class_context( + (Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => { + let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else { + // TODO: emit a diagnostic + return Type::unknown(); + }; + self.legacy_generic_class_context( value_node, - typevars.elements(self.db()), + typevars.elements_slice(), LegacyGenericBase::Generic, ) .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))) - .unwrap_or_else(Type::unknown), + .unwrap_or_else(Type::unknown) + } (Type::SpecialForm(SpecialFormType::Generic), typevar, _) => self .legacy_generic_class_context( value_node, @@ -9167,10 +9190,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { todo_type!("ellipsis literal in type expression") } - ast::Expr::Starred(starred) => { - self.infer_starred_expression(starred); - todo_type!("PEP 646") - } + ast::Expr::Starred(starred) => self.infer_starred_type_expression(starred), + } + } + + fn infer_starred_type_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { + let ast::ExprStarred { + range: _, + node_index: _, + value, + ctx: _, + } = starred; + + let starred_type = self.infer_type_expression(value); + if let Type::Tuple(_) = starred_type { + starred_type + } else { + todo_type!("PEP 646") } } @@ -9225,7 +9261,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match element { - ast::Expr::Starred(_) => true, + ast::Expr::Starred(_) => !matches!(element_ty, Type::Tuple(_)), ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => { let value_ty = if builder.deferred_state.in_string_annotation() { // Using `.expression_type` does not work in string annotations, because @@ -9246,13 +9282,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Expr::Tuple(elements) => { if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts { self.infer_expression(ellipsis); - let result = KnownClass::Tuple - .to_specialized_instance(self.db(), [self.infer_type_expression(element)]); + let result = + TupleType::homogeneous(self.db(), self.infer_type_expression(element)); self.store_expression_type(tuple_slice, result); return result; } - let mut element_types = Vec::with_capacity(elements.len()); + let mut element_types = TupleSpec::with_capacity(elements.len()); // Whether to infer `Todo` for the whole tuple // (see docstring for `element_could_alter_type_of_whole_tuple`) @@ -9262,13 +9298,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let element_ty = self.infer_type_expression(element); return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty, self); - element_types.push(element_ty); + if let ast::Expr::Starred(_) = element { + if let Type::Tuple(inner_tuple) = element_ty { + element_types = + element_types.concat(self.db(), inner_tuple.tuple(self.db())); + } else { + // TODO: emit a diagnostic + } + } else { + element_types.push(element_ty); + } } let ty = if return_todo { todo_type!("PEP 646") } else { - TupleType::from_elements(self.db(), element_types) + Type::tuple(self.db(), TupleType::new(self.db(), element_types)) }; // Here, we store the type for the inner `int, str` tuple-expression, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 715fe51c6df26..f8c0db572d0df 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -5,6 +5,7 @@ use std::marker::PhantomData; use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance}; use crate::place::{Boundness, Place, PlaceAndQualifiers}; +use crate::types::tuple::TupleType; use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance}; use crate::{Db, FxOrderSet}; @@ -12,12 +13,18 @@ pub(super) use synthesized_protocol::SynthesizedProtocolType; impl<'db> Type<'db> { pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { - if class.is_known(db, KnownClass::Any) { - Self::Dynamic(DynamicType::Any) - } else if class.class_literal(db).0.is_protocol(db) { - Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) - } else { - Self::NominalInstance(NominalInstanceType::from_class(class)) + match (class, class.known(db)) { + (_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any), + (ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => { + TupleType::homogeneous(db, Type::unknown()) + } + (ClassType::Generic(alias), Some(KnownClass::Tuple)) => { + Self::tuple(db, TupleType::new(db, alias.specialization(db).tuple(db))) + } + _ if class.class_literal(db).0.is_protocol(db) => { + Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) + } + _ => Self::NominalInstance(NominalInstanceType::from_class(class)), } } @@ -98,11 +105,24 @@ impl<'db> NominalInstanceType<'db> { } pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool { - if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) { + self.is_disjoint_from_nominal_instance_of_class(db, other.class) + } + + // Note that this method only exists so that we can check disjointness between nominal + // instances of `tuple` and some other class. Tuples are currently represented by the + // `Type::Tuple` variant, not `Type::NominalInstance`. We have a TODO to try to remove the + // dedicated `Tuple` variant in favor of `NominalInstance`; if we can do that, then we won't + // need this method, and its logic can be subsumed into `is_disjoint_from`. + pub(super) fn is_disjoint_from_nominal_instance_of_class( + self, + db: &'db dyn Db, + other_class: ClassType, + ) -> bool { + if self.class.is_final(db) && !self.class.is_subclass_of(db, other_class) { return true; } - if other.class.is_final(db) && !other.class.is_subclass_of(db, self.class) { + if other_class.is_final(db) && !other_class.is_subclass_of(db, self.class) { return true; } @@ -116,7 +136,7 @@ impl<'db> NominalInstanceType<'db> { if self_metaclass == type_type { return false; } - let other_metaclass = other.class.metaclass_instance_type(db); + let other_metaclass = other_class.metaclass_instance_type(db); if other_metaclass == type_type { return false; } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 2538a37335c3a..09517ef03e0c6 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -175,8 +175,8 @@ impl ClassInfoConstraintFunction { match classinfo { Type::Tuple(tuple) => { let mut builder = UnionBuilder::new(db); - for element in tuple.elements(db) { - builder = builder.add(self.generate_constraint(db, *element)?); + for element in tuple.tuple(db).all_elements() { + builder = builder.add(self.generate_constraint(db, element)?); } Some(builder.build()) } @@ -540,7 +540,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { match rhs_ty { Type::Tuple(rhs_tuple) => Some(UnionType::from_elements( self.db, - rhs_tuple.elements(self.db), + rhs_tuple.tuple(self.db).all_elements(), )), Type::StringLiteral(string_literal) => Some(UnionType::from_elements( diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 10dfac9c07288..66b5a011cfab6 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -1,8 +1,9 @@ use crate::db::tests::TestDb; use crate::place::{builtins_symbol, known_module_symbol}; +use crate::types::tuple::TupleType; use crate::types::{ BoundMethodType, CallableType, IntersectionBuilder, KnownClass, Parameter, Parameters, - Signature, SpecialFormType, SubclassOfType, TupleType, Type, UnionType, + Signature, SpecialFormType, SubclassOfType, Type, UnionType, }; use crate::{Db, KnownModule}; use hashbrown::HashSet; diff --git a/crates/ty_python_semantic/src/types/slots.rs b/crates/ty_python_semantic/src/types/slots.rs index e5165fb69ba08..2872406385267 100644 --- a/crates/ty_python_semantic/src/types/slots.rs +++ b/crates/ty_python_semantic/src/types/slots.rs @@ -36,7 +36,7 @@ impl SlotsKind { match slots_ty { // __slots__ = ("a", "b") Type::Tuple(tuple) => { - if tuple.elements(db).is_empty() { + if tuple.tuple(db).is_empty() { Self::Empty } else { Self::NotEmpty diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs new file mode 100644 index 0000000000000..4092a53372ccf --- /dev/null +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -0,0 +1,868 @@ +//! Types describing fixed- and variable-length tuples. +//! +//! At runtime, a Python tuple is a fixed-length immutable list of values. There is no restriction +//! on the types of the elements of a tuple value. In the type system, we want to model both +//! "heterogeneous" tuples that have elements of a fixed sequence of specific types, and +//! "homogeneous" tuples that have an unknown number of elements of the same single type. And in +//! fact, we want to model tuples that are a combination of the two ("mixed" tuples), with a +//! heterogeneous prefix and/or suffix, and a homogeneous portion of unknown length in between +//! those. +//! +//! The description of which elements can appear in a `tuple` is called a [`TupleSpec`]. Other +//! things besides `tuple` instances can be described by a tuple spec — for instance, the targets +//! of an unpacking assignment. A `tuple` specialization that includes `Never` as one of its +//! fixed-length elements cannot be instantiated. We reduce the entire `tuple` type down to +//! `Never`. The same is not true of tuple specs in general. (That means that it is [`TupleType`] +//! that adds that "collapse `Never`" behavior, whereas [`TupleSpec`] allows you to add any element +//! types, including `Never`.) + +use itertools::Either; + +use crate::types::class::{ClassType, KnownClass}; +use crate::types::{Type, TypeMapping, TypeRelation, TypeVarInstance, TypeVarVariance, UnionType}; +use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; +use crate::{Db, FxOrderSet}; + +/// # Ordering +/// Ordering is based on the tuple's salsa-assigned id and not on its elements. +/// The id may change between runs, or when the tuple was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct TupleType<'db> { + #[returns(ref)] + pub(crate) tuple: TupleSpec<'db>, +} + +impl<'db> Type<'db> { + pub(crate) fn tuple(db: &'db dyn Db, tuple: TupleType<'db>) -> Self { + // If a fixed-length (i.e., mandatory) element of the tuple is `Never`, then it's not + // possible to instantiate the tuple as a whole. (This is not true of the variable-length + // portion of the tuple, since it can contain no elements.) + if tuple.tuple(db).fixed_elements().any(|ty| ty.is_never()) { + return Type::Never; + } + Self::Tuple(tuple) + } +} + +impl<'db> TupleType<'db> { + pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> { + Type::tuple( + db, + TupleType::new(db, TupleSpec::from(FixedLengthTupleSpec::empty())), + ) + } + + pub(crate) fn from_elements( + db: &'db dyn Db, + types: impl IntoIterator>>, + ) -> Type<'db> { + Type::tuple( + db, + TupleType::new( + db, + TupleSpec::from(FixedLengthTupleSpec::from_elements(types)), + ), + ) + } + + #[cfg(test)] + pub(crate) fn mixed( + db: &'db dyn Db, + prefix: impl IntoIterator>>, + variable: Type<'db>, + suffix: impl IntoIterator>>, + ) -> Type<'db> { + Type::tuple( + db, + TupleType::new( + db, + TupleSpec::from(VariableLengthTupleSpec::mixed(prefix, variable, suffix)), + ), + ) + } + + pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> { + Type::tuple(db, TupleType::new(db, TupleSpec::homogeneous(element))) + } + + pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option> { + KnownClass::Tuple + .try_to_class_literal(db) + .and_then(|class_literal| match class_literal.generic_context(db) { + None => Some(ClassType::NonGeneric(class_literal)), + Some(generic_context) if generic_context.variables(db).len() != 1 => None, + Some(generic_context) => Some( + class_literal + .apply_specialization(db, |_| generic_context.specialize_tuple(db, self)), + ), + }) + } + + /// Return a normalized version of `self`. + /// + /// See [`Type::normalized`] for more details. + #[must_use] + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + TupleType::new(db, self.tuple(db).normalized(db)) + } + + pub(crate) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + TupleType::new(db, self.tuple(db).materialize(db, variance)) + } + + pub(crate) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + TupleType::new(db, self.tuple(db).apply_type_mapping(db, type_mapping)) + } + + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.tuple(db).find_legacy_typevars(db, typevars); + } + + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + self.tuple(db) + .has_relation_to(db, other.tuple(db), relation) + } + + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.tuple(db).is_equivalent_to(db, other.tuple(db)) + } + + pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.tuple(db).is_gradual_equivalent_to(db, other.tuple(db)) + } + + pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool { + self.tuple(db).is_disjoint_from(db, other.tuple(db)) + } + + pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { + self.tuple(db).is_fully_static(db) + } + + pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { + self.tuple(db).is_single_valued(db) + } +} + +/// A fixed-length tuple spec. +/// +/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a +/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that +/// must contain an element that can't be instantiated, can't be instantiated itself). +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, salsa::Update)] +pub struct FixedLengthTupleSpec<'db>(Vec>); + +impl<'db> FixedLengthTupleSpec<'db> { + pub(crate) fn empty() -> Self { + Self::default() + } + + pub(crate) fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + pub(crate) fn from_elements(elements: impl IntoIterator>>) -> Self { + Self(elements.into_iter().map(Into::into).collect()) + } + + pub(crate) fn elements_slice(&self) -> &[Type<'db>] { + &self.0 + } + + pub(crate) fn elements(&self) -> impl Iterator> + '_ { + self.0.iter().copied() + } + + /// Returns the length of this tuple. + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn concat(&self, other: &TupleSpec<'db>) -> TupleSpec<'db> { + match other { + TupleSpec::Fixed(other) => { + let mut elements = Vec::with_capacity(self.0.len() + other.0.len()); + elements.extend_from_slice(&self.0); + elements.extend_from_slice(&other.0); + TupleSpec::Fixed(FixedLengthTupleSpec(elements)) + } + + TupleSpec::Variable(other) => { + let mut prefix = Vec::with_capacity(self.0.len() + other.prefix.len()); + prefix.extend_from_slice(&self.0); + prefix.extend_from_slice(&other.prefix); + TupleSpec::Variable(VariableLengthTupleSpec { + prefix, + variable: other.variable, + suffix: other.suffix.clone(), + }) + } + } + } + + pub(crate) fn push(&mut self, element: Type<'db>) { + self.0.push(element); + } + + pub(crate) fn extend_from_slice(&mut self, elements: &[Type<'db>]) { + self.0.extend_from_slice(elements); + } + + #[must_use] + fn normalized(&self, db: &'db dyn Db) -> Self { + Self(self.0.iter().map(|ty| ty.normalized(db)).collect()) + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self( + self.0 + .iter() + .map(|ty| ty.materialize(db, variance)) + .collect(), + ) + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + Self( + self.0 + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)) + .collect(), + ) + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for ty in &self.0 { + ty.find_legacy_typevars(db, typevars); + } + } + + fn has_relation_to( + &self, + db: &'db dyn Db, + other: &TupleSpec<'db>, + relation: TypeRelation, + ) -> bool { + match other { + TupleSpec::Fixed(other) => { + self.0.len() == other.0.len() + && (self.0.iter()) + .zip(&other.0) + .all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation)) + } + + TupleSpec::Variable(other) => { + // This tuple must have enough elements to match up with the other tuple's prefix + // and suffix, and each of those elements must pairwise satisfy the relation. + let mut self_iter = self.0.iter(); + for other_ty in &other.prefix { + let Some(self_ty) = self_iter.next() else { + return false; + }; + if !self_ty.has_relation_to(db, *other_ty, relation) { + return false; + } + } + for other_ty in other.suffix.iter().rev() { + let Some(self_ty) = self_iter.next_back() else { + return false; + }; + if !self_ty.has_relation_to(db, *other_ty, relation) { + return false; + } + } + + // In addition, any remaining elements in this tuple must satisfy the + // variable-length portion of the other tuple. + self_iter.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation)) + } + } + } + + fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + self.0.len() == other.0.len() + && (self.0.iter()) + .zip(&other.0) + .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) + } + + fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + self.0.len() == other.0.len() + && (self.0.iter()) + .zip(&other.0) + .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) + } + + fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool { + self.0.len() != other.0.len() + || (self.0.iter()) + .zip(&other.0) + .any(|(self_ty, other_ty)| self_ty.is_disjoint_from(db, *other_ty)) + } + + fn is_fully_static(&self, db: &'db dyn Db) -> bool { + self.0.iter().all(|ty| ty.is_fully_static(db)) + } + + fn is_single_valued(&self, db: &'db dyn Db) -> bool { + self.0.iter().all(|ty| ty.is_single_valued(db)) + } +} + +impl<'db> PyIndex<'db> for &FixedLengthTupleSpec<'db> { + type Item = Type<'db>; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + self.0.as_slice().py_index(db, index).copied() + } +} + +impl<'db> PySlice<'db> for FixedLengthTupleSpec<'db> { + type Item = Type<'db>; + + fn py_slice( + &'db self, + db: &'db dyn Db, + start: Option, + stop: Option, + step: Option, + ) -> Result, StepSizeZeroError> { + self.0.py_slice(db, start, stop, step) + } +} + +/// A variable-length tuple spec. +/// +/// The tuple spec can contain a fixed-length heterogeneous prefix and/or suffix. All of the +/// elements of the variable-length portion must be of the same type. +/// +/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a +/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that +/// must contain an element that can't be instantiated, can't be instantiated itself). +#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)] +pub struct VariableLengthTupleSpec<'db> { + pub(crate) prefix: Vec>, + pub(crate) variable: Type<'db>, + pub(crate) suffix: Vec>, +} + +impl<'db> VariableLengthTupleSpec<'db> { + /// Creates a new tuple spec containing zero or more elements of a given type, with no prefix + /// or suffix. + fn homogeneous(ty: Type<'db>) -> Self { + Self { + prefix: vec![], + variable: ty, + suffix: vec![], + } + } + + #[cfg(test)] + fn mixed( + prefix: impl IntoIterator>>, + variable: Type<'db>, + suffix: impl IntoIterator>>, + ) -> Self { + Self { + prefix: prefix.into_iter().map(Into::into).collect(), + variable, + suffix: suffix.into_iter().map(Into::into).collect(), + } + } + + fn fixed_elements(&self) -> impl Iterator> + '_ { + (self.prefix.iter().copied()).chain(self.suffix.iter().copied()) + } + + fn all_elements(&self) -> impl Iterator> + '_ { + (self.prefix.iter().copied()) + .chain(std::iter::once(self.variable)) + .chain(self.suffix.iter().copied()) + } + + /// Returns the minimum length of this tuple. + pub(crate) fn minimum_length(&self) -> usize { + self.prefix.len() + self.suffix.len() + } + + fn concat(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> TupleSpec<'db> { + match other { + TupleSpec::Fixed(other) => { + let mut suffix = Vec::with_capacity(self.suffix.len() + other.0.len()); + suffix.extend_from_slice(&self.suffix); + suffix.extend_from_slice(&other.0); + TupleSpec::Variable(VariableLengthTupleSpec { + prefix: self.prefix.clone(), + variable: self.variable, + suffix, + }) + } + + TupleSpec::Variable(other) => { + let variable = UnionType::from_elements( + db, + (self.suffix.iter().copied()) + .chain([self.variable, other.variable]) + .chain(other.prefix.iter().copied()), + ); + TupleSpec::Variable(VariableLengthTupleSpec { + prefix: self.prefix.clone(), + variable, + suffix: other.suffix.clone(), + }) + } + } + } + + fn push(&mut self, element: Type<'db>) { + self.suffix.push(element); + } + + #[must_use] + fn normalized(&self, db: &'db dyn Db) -> Self { + Self { + prefix: self.prefix.iter().map(|ty| ty.normalized(db)).collect(), + variable: self.variable.normalized(db), + suffix: self.suffix.iter().map(|ty| ty.normalized(db)).collect(), + } + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self { + prefix: self + .prefix + .iter() + .map(|ty| ty.materialize(db, variance)) + .collect(), + variable: self.variable.materialize(db, variance), + suffix: self + .suffix + .iter() + .map(|ty| ty.materialize(db, variance)) + .collect(), + } + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + Self { + prefix: self + .prefix + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)) + .collect(), + variable: self.variable.apply_type_mapping(db, type_mapping), + suffix: self + .suffix + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)) + .collect(), + } + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for ty in &self.prefix { + ty.find_legacy_typevars(db, typevars); + } + self.variable.find_legacy_typevars(db, typevars); + for ty in &self.suffix { + ty.find_legacy_typevars(db, typevars); + } + } + + fn has_relation_to( + &self, + db: &'db dyn Db, + other: &TupleSpec<'db>, + relation: TypeRelation, + ) -> bool { + match other { + TupleSpec::Fixed(other) => { + // The `...` length specifier of a variable-length tuple type is interpreted + // differently depending on the type of the variable-length elements. + // + // It typically represents the _union_ of all possible lengths. That means that a + // variable-length tuple type is not a subtype of _any_ fixed-length tuple type. + // + // However, as a special case, if the variable-length portion of the tuple is `Any` + // (or any other dynamic type), then the `...` is the _gradual choice_ of all + // possible lengths. This means that `tuple[Any, ...]` can match any tuple of any + // length. + if relation == TypeRelation::Subtyping || !matches!(self.variable, Type::Dynamic(_)) + { + return false; + } + + // In addition, the other tuple must have enough elements to match up with this + // tuple's prefix and suffix, and each of those elements must pairwise satisfy the + // relation. + let mut other_iter = other.0.iter(); + for self_ty in &self.prefix { + let Some(other_ty) = other_iter.next() else { + return false; + }; + if !self_ty.has_relation_to(db, *other_ty, relation) { + return false; + } + } + for self_ty in self.suffix.iter().rev() { + let Some(other_ty) = other_iter.next_back() else { + return false; + }; + if !self_ty.has_relation_to(db, *other_ty, relation) { + return false; + } + } + + true + } + + TupleSpec::Variable(other) => { + // The overlapping parts of the prefixes and suffixes must satisfy the relation. + let mut self_prefix = self.prefix.iter(); + let mut other_prefix = other.prefix.iter(); + let prefixes_match = (&mut self_prefix) + .zip(&mut other_prefix) + .all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation)); + if !prefixes_match { + return false; + } + + let mut self_suffix = self.suffix.iter().rev(); + let mut other_suffix = other.suffix.iter().rev(); + let suffixes_match = (&mut self_suffix) + .zip(&mut other_suffix) + .all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation)); + if !suffixes_match { + return false; + } + + // Any remaining parts of either prefix or suffix must satisfy the relation with + // the other tuple's variable-length portion. + let prefix_matches_variable = self_prefix + .all(|self_ty| self_ty.has_relation_to(db, other.variable, relation)); + if !prefix_matches_variable { + return false; + } + let prefix_matches_variable = other_prefix + .all(|other_ty| self.variable.has_relation_to(db, *other_ty, relation)); + if !prefix_matches_variable { + return false; + } + + let suffix_matches_variable = self_suffix + .all(|self_ty| self_ty.has_relation_to(db, other.variable, relation)); + if !suffix_matches_variable { + return false; + } + let suffix_matches_variable = other_suffix + .all(|other_ty| self.variable.has_relation_to(db, *other_ty, relation)); + if !suffix_matches_variable { + return false; + } + + // And lastly, the variable-length portions must satisfy the relation. + self.variable.has_relation_to(db, other.variable, relation) + } + } + } + + fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + self.prefix.len() == other.prefix.len() + && self.suffix.len() == other.suffix.len() + && self.variable.is_equivalent_to(db, other.variable) + && (self.prefix.iter()) + .zip(&other.prefix) + .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) + && (self.suffix.iter()) + .zip(&other.suffix) + .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) + } + + fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + self.prefix.len() == other.prefix.len() + && self.suffix.len() == other.suffix.len() + && self.variable.is_gradual_equivalent_to(db, other.variable) + && (self.prefix.iter()) + .zip(&other.prefix) + .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) + && (self.suffix.iter()) + .zip(&other.suffix) + .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) + } + + fn is_fully_static(&self, db: &'db dyn Db) -> bool { + self.variable.is_fully_static(db) + && self.prefix.iter().all(|ty| ty.is_fully_static(db)) + && self.suffix.iter().all(|ty| ty.is_fully_static(db)) + } +} + +impl<'db> PyIndex<'db> for &VariableLengthTupleSpec<'db> { + type Item = Type<'db>; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + match Nth::from_index(index) { + Nth::FromStart(index) => { + if let Some(element) = self.prefix.get(index) { + // index is small enough that it lands in the prefix of the tuple. + return Ok(*element); + } + + // index is large enough that it lands past the prefix. The tuple can always be + // large enough that it lands in the variable-length portion. It might also be + // small enough to land in the suffix. + let index_past_prefix = index - self.prefix.len() + 1; + Ok(UnionType::from_elements( + db, + std::iter::once(self.variable) + .chain(self.suffix.iter().copied().take(index_past_prefix)), + )) + } + + Nth::FromEnd(index_from_end) => { + if index_from_end < self.suffix.len() { + // index is small enough that it lands in the suffix of the tuple. + return Ok(self.suffix[self.suffix.len() - index_from_end - 1]); + } + + // index is large enough that it lands past the suffix. The tuple can always be + // large enough that it lands in the variable-length portion. It might also be + // small enough to land in the prefix. + let index_past_suffix = index_from_end - self.suffix.len() + 1; + Ok(UnionType::from_elements( + db, + (self.prefix.iter().rev().copied()) + .take(index_past_suffix) + .rev() + .chain(std::iter::once(self.variable)), + )) + } + } + } +} + +/// A tuple spec that might be fixed- or variable-length. +/// +/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a +/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that +/// must contain an element that can't be instantiated, can't be instantiated itself). +#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)] +pub enum TupleSpec<'db> { + Fixed(FixedLengthTupleSpec<'db>), + Variable(VariableLengthTupleSpec<'db>), +} + +impl<'db> TupleSpec<'db> { + pub(crate) fn with_capacity(capacity: usize) -> Self { + TupleSpec::Fixed(FixedLengthTupleSpec::with_capacity(capacity)) + } + + pub(crate) fn homogeneous(element: Type<'db>) -> Self { + TupleSpec::from(VariableLengthTupleSpec::homogeneous(element)) + } + + /// Returns an iterator of all of the fixed-length element types of this tuple. + pub(crate) fn fixed_elements(&self) -> impl Iterator> + '_ { + match self { + TupleSpec::Fixed(tuple) => Either::Left(tuple.elements()), + TupleSpec::Variable(tuple) => Either::Right(tuple.fixed_elements()), + } + } + + /// Returns an iterator of all of the element types of this tuple. Does not deduplicate the + /// elements, and does not distinguish between fixed- and variable-length elements. + pub(crate) fn all_elements(&self) -> impl Iterator> + '_ { + match self { + TupleSpec::Fixed(tuple) => Either::Left(tuple.elements()), + TupleSpec::Variable(tuple) => Either::Right(tuple.all_elements()), + } + } + + pub(crate) fn display_minimum_length(&self) -> String { + match self { + TupleSpec::Fixed(tuple) => tuple.len().to_string(), + TupleSpec::Variable(tuple) => format!("at least {}", tuple.minimum_length()), + } + } + + /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` + /// for a tuple with a variable-length portion.) + pub(crate) fn size_hint(&self) -> (usize, Option) { + match self { + TupleSpec::Fixed(tuple) => { + let len = tuple.len(); + (len, Some(len)) + } + TupleSpec::Variable(tuple) => (tuple.minimum_length(), None), + } + } + + pub(crate) fn is_empty(&self) -> bool { + match self { + TupleSpec::Fixed(tuple) => tuple.is_empty(), + TupleSpec::Variable(_) => false, + } + } + + /// Concatenates another tuple to the end of this tuple, returning a new tuple. + pub(crate) fn concat(&self, db: &'db dyn Db, other: &Self) -> Self { + match self { + TupleSpec::Fixed(tuple) => tuple.concat(other), + TupleSpec::Variable(tuple) => tuple.concat(db, other), + } + } + + pub(crate) fn push(&mut self, element: Type<'db>) { + match self { + TupleSpec::Fixed(tuple) => tuple.push(element), + TupleSpec::Variable(tuple) => tuple.push(element), + } + } + + fn normalized(&self, db: &'db dyn Db) -> Self { + match self { + TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.normalized(db)), + TupleSpec::Variable(tuple) => TupleSpec::Variable(tuple.normalized(db)), + } + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + match self { + TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.materialize(db, variance)), + TupleSpec::Variable(tuple) => TupleSpec::Variable(tuple.materialize(db, variance)), + } + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + match self { + TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.apply_type_mapping(db, type_mapping)), + TupleSpec::Variable(tuple) => { + TupleSpec::Variable(tuple.apply_type_mapping(db, type_mapping)) + } + } + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + TupleSpec::Fixed(tuple) => tuple.find_legacy_typevars(db, typevars), + TupleSpec::Variable(tuple) => tuple.find_legacy_typevars(db, typevars), + } + } + + fn has_relation_to(&self, db: &'db dyn Db, other: &Self, relation: TypeRelation) -> bool { + match self { + TupleSpec::Fixed(self_tuple) => self_tuple.has_relation_to(db, other, relation), + TupleSpec::Variable(self_tuple) => self_tuple.has_relation_to(db, other, relation), + } + } + + fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + match (self, other) { + (TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => { + self_tuple.is_equivalent_to(db, other_tuple) + } + (TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => { + self_tuple.is_equivalent_to(db, other_tuple) + } + (TupleSpec::Fixed(_), TupleSpec::Variable(_)) + | (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false, + } + } + + fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> bool { + match (self, other) { + (TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => { + self_tuple.is_gradual_equivalent_to(db, other_tuple) + } + (TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => { + self_tuple.is_gradual_equivalent_to(db, other_tuple) + } + (TupleSpec::Fixed(_), TupleSpec::Variable(_)) + | (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false, + } + } + + fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool { + match (self, other) { + (TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => { + self_tuple.is_disjoint_from(db, other_tuple) + } + // Two pure homogeneous tuples `tuple[A, ...]` and `tuple[B, ...]` can never be + // disjoint even if A and B are disjoint, because `tuple[()]` would be assignable to + // both. + // TODO: Consider checking for disjointness between the tuples' prefixes and suffixes. + (TupleSpec::Variable(_), TupleSpec::Variable(_)) => false, + // TODO: Consider checking for disjointness between the fixed-length tuple and the + // variable-length tuple's prefix/suffix. + (TupleSpec::Fixed(_), TupleSpec::Variable(_)) + | (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false, + } + } + + fn is_fully_static(&self, db: &'db dyn Db) -> bool { + match self { + TupleSpec::Fixed(tuple) => tuple.is_fully_static(db), + TupleSpec::Variable(tuple) => tuple.is_fully_static(db), + } + } + + fn is_single_valued(&self, db: &'db dyn Db) -> bool { + match self { + TupleSpec::Fixed(tuple) => tuple.is_single_valued(db), + TupleSpec::Variable(_) => false, + } + } +} + +impl<'db> From> for TupleSpec<'db> { + fn from(tuple: FixedLengthTupleSpec<'db>) -> Self { + TupleSpec::Fixed(tuple) + } +} + +impl<'db> From> for TupleSpec<'db> { + fn from(tuple: VariableLengthTupleSpec<'db>) -> Self { + TupleSpec::Variable(tuple) + } +} + +impl<'db> PyIndex<'db> for &TupleSpec<'db> { + type Item = Type<'db>; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + match self { + TupleSpec::Fixed(tuple) => tuple.py_index(db, index), + TupleSpec::Variable(tuple) => tuple.py_index(db, index), + } + } +} diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index af40005580b35..f963fb02550e4 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -9,12 +9,13 @@ use ruff_python_ast::{self as ast, AnyNodeRef}; use crate::Db; use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId}; use crate::semantic_index::place::ScopeId; -use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types}; +use crate::types::tuple::{FixedLengthTupleSpec, TupleSpec, TupleType}; +use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types, todo_type}; use crate::unpack::{UnpackKind, UnpackValue}; use super::context::InferContext; use super::diagnostic::INVALID_ASSIGNMENT; -use super::{KnownClass, TupleType, UnionType}; +use super::{KnownClass, UnionType}; /// Unpacks the value expression type to their respective targets. pub(crate) struct Unpacker<'db, 'ast> { @@ -152,53 +153,55 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { _ => ty, }; - if let Some(tuple_ty) = ty.into_tuple() { - let tuple_ty_elements = - self.tuple_ty_elements(target, elts, tuple_ty, value_expr); - - let length_mismatch = - match elts.len().cmp(&tuple_ty_elements.len()) { - Ordering::Less => { - if let Some(builder) = - self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - let mut diag = - builder.into_diagnostic("Too many values to unpack"); - diag.set_primary_message(format_args!( - "Expected {}", - elts.len(), - )); - diag.annotate(self.context.secondary(value_expr).message( - format_args!("Got {}", tuple_ty_elements.len()), - )); - } - true + if let Type::Tuple(tuple_ty) = ty { + let tuple = self.tuple_ty_elements(target, elts, tuple_ty, value_expr); + + let length_mismatch = match elts.len().cmp(&tuple.len()) { + Ordering::Less => { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + let mut diag = + builder.into_diagnostic("Too many values to unpack"); + diag.set_primary_message(format_args!( + "Expected {}", + elts.len(), + )); + diag.annotate( + self.context + .secondary(value_expr) + .message(format_args!("Got {}", tuple.len())), + ); } - Ordering::Greater => { - if let Some(builder) = - self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - let mut diag = - builder.into_diagnostic("Not enough values to unpack"); - diag.set_primary_message(format_args!( - "Expected {}", - elts.len(), - )); - diag.annotate(self.context.secondary(value_expr).message( - format_args!("Got {}", tuple_ty_elements.len()), - )); - } - true + true + } + Ordering::Greater => { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + let mut diag = + builder.into_diagnostic("Not enough values to unpack"); + diag.set_primary_message(format_args!( + "Expected {}", + elts.len(), + )); + diag.annotate( + self.context + .secondary(value_expr) + .message(format_args!("Got {}", tuple.len())), + ); } - Ordering::Equal => false, - }; + true + } + Ordering::Equal => false, + }; - for (index, ty) in tuple_ty_elements.iter().enumerate() { + for (index, ty) in tuple.elements().enumerate() { if let Some(element_types) = target_types.get_mut(index) { if length_mismatch { element_types.push(Type::unknown()); } else { - element_types.push(*ty); + element_types.push(ty); } } } @@ -248,24 +251,36 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { targets: &[ast::Expr], tuple_ty: TupleType<'db>, value_expr: AnyNodeRef<'_>, - ) -> Cow<'_, [Type<'db>]> { + ) -> Cow<'_, FixedLengthTupleSpec<'db>> { + let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else { + let todo = todo_type!("Unpack variable-length tuple"); + return Cow::Owned(FixedLengthTupleSpec::from_elements(targets.iter().map( + |target| { + if target.is_starred_expr() { + KnownClass::List.to_specialized_instance(self.db(), [todo]) + } else { + todo + } + }, + ))); + }; + // If there is a starred expression, it will consume all of the types at that location. let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else { // Otherwise, the types will be unpacked 1-1 to the targets. - return Cow::Borrowed(tuple_ty.elements(self.db())); + return Cow::Borrowed(tuple); }; - if tuple_ty.len(self.db()) >= targets.len() - 1 { + if tuple.len() >= targets.len() - 1 { // This branch is only taken when there are enough elements in the tuple type to // combine for the starred expression. So, the arithmetic and indexing operations are // safe to perform. - let mut element_types = Vec::with_capacity(targets.len()); + let mut element_types = FixedLengthTupleSpec::with_capacity(targets.len()); + let tuple_elements = tuple.elements_slice(); // Insert all the elements before the starred expression. - element_types.extend_from_slice( - // SAFETY: Safe because of the length check above. - &tuple_ty.elements(self.db())[..starred_index], - ); + // SAFETY: Safe because of the length check above. + element_types.extend_from_slice(&tuple_elements[..starred_index]); // The number of target expressions that are remaining after the starred expression. // For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the @@ -276,11 +291,10 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { // expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the // starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and // the index value would be 3. - let starred_end_index = tuple_ty.len(self.db()) - remaining; + let starred_end_index = tuple.len() - remaining; // SAFETY: Safe because of the length check above. - let starred_element_types = - &tuple_ty.elements(self.db())[starred_index..starred_end_index]; + let starred_element_types = &tuple_elements[starred_index..starred_end_index]; element_types.push(KnownClass::List.to_specialized_instance( self.db(), @@ -292,10 +306,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { )); // Insert the types remaining that aren't consumed by the starred expression. - element_types.extend_from_slice( - // SAFETY: Safe because of the length check above. - &tuple_ty.elements(self.db())[starred_end_index..], - ); + // SAFETY: Safe because of the length check above. + element_types.extend_from_slice(&tuple_elements[starred_end_index..]); Cow::Owned(element_types) } else { @@ -305,22 +317,19 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { diag.annotate( self.context .secondary(value_expr) - .message(format_args!("Got {}", tuple_ty.len(self.db()))), + .message(format_args!("Got {}", tuple.len())), ); } - Cow::Owned( - targets - .iter() - .map(|target| { - if target.is_starred_expr() { - KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()]) - } else { - Type::unknown() - } - }) - .collect(), - ) + Cow::Owned(FixedLengthTupleSpec::from_elements(targets.iter().map( + |target| { + if target.is_starred_expr() { + KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()]) + } else { + Type::unknown() + } + }, + ))) } } diff --git a/crates/ty_python_semantic/src/util/subscript.rs b/crates/ty_python_semantic/src/util/subscript.rs index 92765faf716a7..3f05384d8e1d1 100644 --- a/crates/ty_python_semantic/src/util/subscript.rs +++ b/crates/ty_python_semantic/src/util/subscript.rs @@ -4,13 +4,15 @@ use itertools::Either; +use crate::Db; + #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct OutOfBoundsError; -pub(crate) trait PyIndex { - type Item; +pub(crate) trait PyIndex<'db> { + type Item: 'db; - fn py_index(&mut self, index: i32) -> Result; + fn py_index(self, db: &'db dyn Db, index: i32) -> Result; } fn from_nonnegative_i32(index: i32) -> usize { @@ -39,13 +41,13 @@ enum Position { AfterEnd, } -enum Nth { +pub(crate) enum Nth { FromStart(usize), FromEnd(usize), } impl Nth { - fn from_index(index: i32) -> Self { + pub(crate) fn from_index(index: i32) -> Self { if index >= 0 { Nth::FromStart(from_nonnegative_i32(index)) } else { @@ -75,13 +77,26 @@ impl Nth { } } -impl PyIndex for T +impl<'db, T> PyIndex<'db> for &'db [T] { + type Item = &'db T; + + fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<&'db T, OutOfBoundsError> { + match Nth::from_index(index) { + Nth::FromStart(nth) => self.get(nth).ok_or(OutOfBoundsError), + Nth::FromEnd(nth_rev) => (self.len().checked_sub(nth_rev + 1)) + .map(|idx| &self[idx]) + .ok_or(OutOfBoundsError), + } + } +} + +impl<'db, I: 'db, T> PyIndex<'db> for &mut T where T: DoubleEndedIterator, { type Item = I; - fn py_index(&mut self, index: i32) -> Result { + fn py_index(self, _db: &'db dyn Db, index: i32) -> Result { match Nth::from_index(index) { Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError), Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError), @@ -92,32 +107,28 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct StepSizeZeroError; -pub(crate) trait PySlice { - type Item; +pub(crate) trait PySlice<'db> { + type Item: 'db; fn py_slice( - &self, + &'db self, + db: &'db dyn Db, start: Option, stop: Option, step: Option, - ) -> Result< - Either, impl Iterator>, - StepSizeZeroError, - >; + ) -> Result, StepSizeZeroError>; } -impl PySlice for [T] { +impl<'db, T: 'db> PySlice<'db> for [T] { type Item = T; fn py_slice( - &self, + &'db self, + _db: &'db dyn Db, start: Option, stop: Option, step_int: Option, - ) -> Result< - Either, impl Iterator>, - StepSizeZeroError, - > { + ) -> Result, StepSizeZeroError> { let step_int = step_int.unwrap_or(1); if step_int == 0 { return Err(StepSizeZeroError); @@ -194,6 +205,8 @@ impl PySlice for [T] { #[cfg(test)] #[expect(clippy::redundant_clone)] mod tests { + use crate::Db; + use crate::db::tests::setup_db; use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError}; use super::{PyIndex, PySlice}; @@ -201,302 +214,387 @@ mod tests { #[test] fn py_index_empty() { + let db = setup_db(); let iter = std::iter::empty::(); - assert_eq!(iter.clone().py_index(0), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(-1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(i32::MIN), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(i32::MAX), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, 0), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, -1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, i32::MIN), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, i32::MAX), Err(OutOfBoundsError)); } #[test] fn py_index_single_element() { + let db = setup_db(); let iter = ['a'].into_iter(); - assert_eq!(iter.clone().py_index(0), Ok('a')); - assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(-1), Ok('a')); - assert_eq!(iter.clone().py_index(-2), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, 0), Ok('a')); + assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, -1), Ok('a')); + assert_eq!(iter.clone().py_index(&db, -2), Err(OutOfBoundsError)); } #[test] fn py_index_more_elements() { + let db = setup_db(); let iter = ['a', 'b', 'c', 'd', 'e'].into_iter(); - assert_eq!(iter.clone().py_index(0), Ok('a')); - assert_eq!(iter.clone().py_index(1), Ok('b')); - assert_eq!(iter.clone().py_index(4), Ok('e')); - assert_eq!(iter.clone().py_index(5), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, 0), Ok('a')); + assert_eq!(iter.clone().py_index(&db, 1), Ok('b')); + assert_eq!(iter.clone().py_index(&db, 4), Ok('e')); + assert_eq!(iter.clone().py_index(&db, 5), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(-1), Ok('e')); - assert_eq!(iter.clone().py_index(-2), Ok('d')); - assert_eq!(iter.clone().py_index(-5), Ok('a')); - assert_eq!(iter.clone().py_index(-6), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, -1), Ok('e')); + assert_eq!(iter.clone().py_index(&db, -2), Ok('d')); + assert_eq!(iter.clone().py_index(&db, -5), Ok('a')); + assert_eq!(iter.clone().py_index(&db, -6), Err(OutOfBoundsError)); } #[test] fn py_index_uses_full_index_range() { + let db = setup_db(); let iter = 0..=u32::MAX; // u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31 - assert_eq!(iter.clone().py_index(i32::MIN), Ok(2u32.pow(31))); - assert_eq!(iter.clone().py_index(-2), Ok(u32::MAX - 2 + 1)); - assert_eq!(iter.clone().py_index(-1), Ok(u32::MAX - 1 + 1)); + assert_eq!(iter.clone().py_index(&db, i32::MIN), Ok(2u32.pow(31))); + assert_eq!(iter.clone().py_index(&db, -2), Ok(u32::MAX - 2 + 1)); + assert_eq!(iter.clone().py_index(&db, -1), Ok(u32::MAX - 1 + 1)); - assert_eq!(iter.clone().py_index(0), Ok(0)); - assert_eq!(iter.clone().py_index(1), Ok(1)); - assert_eq!(iter.clone().py_index(i32::MAX), Ok(i32::MAX as u32)); + assert_eq!(iter.clone().py_index(&db, 0), Ok(0)); + assert_eq!(iter.clone().py_index(&db, 1), Ok(1)); + assert_eq!(iter.clone().py_index(&db, i32::MAX), Ok(i32::MAX as u32)); } #[track_caller] fn assert_eq_slice( + db: &dyn Db, input: &[char; N], start: Option, stop: Option, step: Option, expected: &[char; M], ) { - assert_equal(input.py_slice(start, stop, step).unwrap(), expected.iter()); + assert_equal( + input.py_slice(db, start, stop, step).unwrap(), + expected.iter(), + ); } #[test] fn py_slice_empty_input() { + let db = setup_db(); let input = []; - assert_eq_slice(&input, None, None, None, &[]); - assert_eq_slice(&input, Some(0), None, None, &[]); - assert_eq_slice(&input, None, Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(0), None, &[]); - assert_eq_slice(&input, Some(-5), Some(-5), None, &[]); - assert_eq_slice(&input, None, None, Some(-1), &[]); - assert_eq_slice(&input, None, None, Some(2), &[]); + assert_eq_slice(&db, &input, None, None, None, &[]); + assert_eq_slice(&db, &input, Some(0), None, None, &[]); + assert_eq_slice(&db, &input, None, Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]); + assert_eq_slice(&db, &input, None, None, Some(-1), &[]); + assert_eq_slice(&db, &input, None, None, Some(2), &[]); } #[test] fn py_slice_single_element_input() { + let db = setup_db(); let input = ['a']; - assert_eq_slice(&input, None, None, None, &['a']); - - assert_eq_slice(&input, Some(0), None, None, &['a']); - assert_eq_slice(&input, None, Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(1), None, &['a']); - assert_eq_slice(&input, Some(0), Some(2), None, &['a']); - - assert_eq_slice(&input, Some(-1), None, None, &['a']); - assert_eq_slice(&input, Some(-1), Some(-1), None, &[]); - assert_eq_slice(&input, Some(-1), Some(0), None, &[]); - assert_eq_slice(&input, Some(-1), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-1), Some(2), None, &['a']); - assert_eq_slice(&input, None, Some(-1), None, &[]); - - assert_eq_slice(&input, Some(-2), None, None, &['a']); - assert_eq_slice(&input, Some(-2), Some(-1), None, &[]); - assert_eq_slice(&input, Some(-2), Some(0), None, &[]); - assert_eq_slice(&input, Some(-2), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-2), Some(2), None, &['a']); + assert_eq_slice(&db, &input, None, None, None, &['a']); + + assert_eq_slice(&db, &input, Some(0), None, None, &['a']); + assert_eq_slice(&db, &input, None, Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(0), Some(2), None, &['a']); + + assert_eq_slice(&db, &input, Some(-1), None, None, &['a']); + assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-1), Some(2), None, &['a']); + assert_eq_slice(&db, &input, None, Some(-1), None, &[]); + + assert_eq_slice(&db, &input, Some(-2), None, None, &['a']); + assert_eq_slice(&db, &input, Some(-2), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(-2), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(-2), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-2), Some(2), None, &['a']); } #[test] fn py_slice_nonnegative_indices() { + let db = setup_db(); let input = ['a', 'b', 'c', 'd', 'e']; - assert_eq_slice(&input, None, Some(0), None, &[]); - assert_eq_slice(&input, None, Some(1), None, &['a']); - assert_eq_slice(&input, None, Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, None, None, None, &['a', 'b', 'c', 'd', 'e']); - - assert_eq_slice(&input, Some(0), Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(1), None, &['a']); - assert_eq_slice(&input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(0), Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(0), Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']); - - assert_eq_slice(&input, Some(1), Some(0), None, &[]); - assert_eq_slice(&input, Some(1), Some(1), None, &[]); - assert_eq_slice(&input, Some(1), Some(2), None, &['b']); - assert_eq_slice(&input, Some(1), Some(4), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(1), None, None, &['b', 'c', 'd', 'e']); - - assert_eq_slice(&input, Some(4), Some(0), None, &[]); - assert_eq_slice(&input, Some(4), Some(4), None, &[]); - assert_eq_slice(&input, Some(4), Some(5), None, &['e']); - assert_eq_slice(&input, Some(4), Some(6), None, &['e']); - assert_eq_slice(&input, Some(4), None, None, &['e']); - - assert_eq_slice(&input, Some(5), Some(0), None, &[]); - assert_eq_slice(&input, Some(5), Some(5), None, &[]); - assert_eq_slice(&input, Some(5), Some(6), None, &[]); - assert_eq_slice(&input, Some(5), None, None, &[]); - - assert_eq_slice(&input, Some(6), Some(0), None, &[]); - assert_eq_slice(&input, Some(6), Some(6), None, &[]); - assert_eq_slice(&input, Some(6), None, None, &[]); + assert_eq_slice(&db, &input, None, Some(0), None, &[]); + assert_eq_slice(&db, &input, None, Some(1), None, &['a']); + assert_eq_slice(&db, &input, None, Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, None, None, None, &['a', 'b', 'c', 'd', 'e']); + + assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice( + &db, + &input, + Some(0), + Some(5), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice( + &db, + &input, + Some(0), + Some(6), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']); + + assert_eq_slice(&db, &input, Some(1), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(1), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(2), None, &['b']); + assert_eq_slice(&db, &input, Some(1), Some(4), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(1), None, None, &['b', 'c', 'd', 'e']); + + assert_eq_slice(&db, &input, Some(4), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(4), Some(4), None, &[]); + assert_eq_slice(&db, &input, Some(4), Some(5), None, &['e']); + assert_eq_slice(&db, &input, Some(4), Some(6), None, &['e']); + assert_eq_slice(&db, &input, Some(4), None, None, &['e']); + + assert_eq_slice(&db, &input, Some(5), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(5), Some(5), None, &[]); + assert_eq_slice(&db, &input, Some(5), Some(6), None, &[]); + assert_eq_slice(&db, &input, Some(5), None, None, &[]); + + assert_eq_slice(&db, &input, Some(6), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(6), Some(6), None, &[]); + assert_eq_slice(&db, &input, Some(6), None, None, &[]); } #[test] fn py_slice_negative_indices() { + let db = setup_db(); let input = ['a', 'b', 'c', 'd', 'e']; - assert_eq_slice(&input, Some(-6), None, None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-6), Some(-4), None, &['a']); - assert_eq_slice(&input, Some(-6), Some(-5), None, &[]); - assert_eq_slice(&input, Some(-6), Some(-6), None, &[]); - assert_eq_slice(&input, Some(-6), Some(-10), None, &[]); - - assert_eq_slice(&input, Some(-5), None, None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-5), Some(-4), None, &['a']); - assert_eq_slice(&input, Some(-5), Some(-5), None, &[]); - assert_eq_slice(&input, Some(-5), Some(-6), None, &[]); - assert_eq_slice(&input, Some(-5), Some(-10), None, &[]); - - assert_eq_slice(&input, Some(-4), None, None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-4), Some(-1), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(-4), Some(-3), None, &['b']); - assert_eq_slice(&input, Some(-4), Some(-4), None, &[]); - assert_eq_slice(&input, Some(-4), Some(-10), None, &[]); - - assert_eq_slice(&input, Some(-1), None, None, &['e']); - assert_eq_slice(&input, Some(-1), Some(-1), None, &[]); - assert_eq_slice(&input, Some(-1), Some(-10), None, &[]); - - assert_eq_slice(&input, None, Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, None, Some(-4), None, &['a']); - assert_eq_slice(&input, None, Some(-5), None, &[]); - assert_eq_slice(&input, None, Some(-6), None, &[]); + assert_eq_slice( + &db, + &input, + Some(-6), + None, + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-6), Some(-4), None, &['a']); + assert_eq_slice(&db, &input, Some(-6), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(-6), Some(-6), None, &[]); + assert_eq_slice(&db, &input, Some(-6), Some(-10), None, &[]); + + assert_eq_slice( + &db, + &input, + Some(-5), + None, + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-5), Some(-4), None, &['a']); + assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(-5), Some(-6), None, &[]); + assert_eq_slice(&db, &input, Some(-5), Some(-10), None, &[]); + + assert_eq_slice(&db, &input, Some(-4), None, None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(-4), Some(-1), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-4), Some(-3), None, &['b']); + assert_eq_slice(&db, &input, Some(-4), Some(-4), None, &[]); + assert_eq_slice(&db, &input, Some(-4), Some(-10), None, &[]); + + assert_eq_slice(&db, &input, Some(-1), None, None, &['e']); + assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(-10), None, &[]); + + assert_eq_slice(&db, &input, None, Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, None, Some(-4), None, &['a']); + assert_eq_slice(&db, &input, None, Some(-5), None, &[]); + assert_eq_slice(&db, &input, None, Some(-6), None, &[]); } #[test] fn py_slice_mixed_positive_negative_indices() { + let db = setup_db(); let input = ['a', 'b', 'c', 'd', 'e']; - assert_eq_slice(&input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(1), Some(-1), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(3), Some(-1), None, &['d']); - assert_eq_slice(&input, Some(4), Some(-1), None, &[]); - assert_eq_slice(&input, Some(5), Some(-1), None, &[]); - - assert_eq_slice(&input, Some(0), Some(-4), None, &['a']); - assert_eq_slice(&input, Some(1), Some(-4), None, &[]); - assert_eq_slice(&input, Some(3), Some(-4), None, &[]); - - assert_eq_slice(&input, Some(0), Some(-5), None, &[]); - assert_eq_slice(&input, Some(1), Some(-5), None, &[]); - assert_eq_slice(&input, Some(3), Some(-5), None, &[]); - - assert_eq_slice(&input, Some(0), Some(-6), None, &[]); - assert_eq_slice(&input, Some(1), Some(-6), None, &[]); - - assert_eq_slice(&input, Some(-6), Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-6), Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-6), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-6), Some(0), None, &[]); - - assert_eq_slice(&input, Some(-5), Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-5), Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-5), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-5), Some(0), None, &[]); - - assert_eq_slice(&input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-4), Some(4), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(-4), Some(2), None, &['b']); - assert_eq_slice(&input, Some(-4), Some(1), None, &[]); - assert_eq_slice(&input, Some(-4), Some(0), None, &[]); - - assert_eq_slice(&input, Some(-1), Some(6), None, &['e']); - assert_eq_slice(&input, Some(-1), Some(5), None, &['e']); - assert_eq_slice(&input, Some(-1), Some(4), None, &[]); - assert_eq_slice(&input, Some(-1), Some(1), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(1), Some(-1), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(3), Some(-1), None, &['d']); + assert_eq_slice(&db, &input, Some(4), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(5), Some(-1), None, &[]); + + assert_eq_slice(&db, &input, Some(0), Some(-4), None, &['a']); + assert_eq_slice(&db, &input, Some(1), Some(-4), None, &[]); + assert_eq_slice(&db, &input, Some(3), Some(-4), None, &[]); + + assert_eq_slice(&db, &input, Some(0), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(3), Some(-5), None, &[]); + + assert_eq_slice(&db, &input, Some(0), Some(-6), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(-6), None, &[]); + + assert_eq_slice( + &db, + &input, + Some(-6), + Some(6), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice( + &db, + &input, + Some(-6), + Some(5), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-6), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-6), Some(0), None, &[]); + + assert_eq_slice( + &db, + &input, + Some(-5), + Some(6), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice( + &db, + &input, + Some(-5), + Some(5), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-5), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-5), Some(0), None, &[]); + + assert_eq_slice(&db, &input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(-4), Some(4), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-4), Some(2), None, &['b']); + assert_eq_slice(&db, &input, Some(-4), Some(1), None, &[]); + assert_eq_slice(&db, &input, Some(-4), Some(0), None, &[]); + + assert_eq_slice(&db, &input, Some(-1), Some(6), None, &['e']); + assert_eq_slice(&db, &input, Some(-1), Some(5), None, &['e']); + assert_eq_slice(&db, &input, Some(-1), Some(4), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(1), None, &[]); } #[test] fn py_slice_step_forward() { + let db = setup_db(); // indices: 0 1 2 3 4 5 6 let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; // Step size zero is invalid: assert!(matches!( - input.py_slice(None, None, Some(0)), + input.py_slice(&db, None, None, Some(0)), Err(StepSizeZeroError) )); assert!(matches!( - input.py_slice(Some(0), Some(5), Some(0)), + input.py_slice(&db, Some(0), Some(5), Some(0)), Err(StepSizeZeroError) )); assert!(matches!( - input.py_slice(Some(0), Some(0), Some(0)), + input.py_slice(&db, Some(0), Some(0), Some(0)), Err(StepSizeZeroError) )); - assert_eq_slice(&input, Some(0), Some(8), Some(2), &['a', 'c', 'e', 'g']); - assert_eq_slice(&input, Some(0), Some(7), Some(2), &['a', 'c', 'e', 'g']); - assert_eq_slice(&input, Some(0), Some(6), Some(2), &['a', 'c', 'e']); - assert_eq_slice(&input, Some(0), Some(5), Some(2), &['a', 'c', 'e']); - assert_eq_slice(&input, Some(0), Some(4), Some(2), &['a', 'c']); - assert_eq_slice(&input, Some(0), Some(3), Some(2), &['a', 'c']); - assert_eq_slice(&input, Some(0), Some(2), Some(2), &['a']); - assert_eq_slice(&input, Some(0), Some(1), Some(2), &['a']); - assert_eq_slice(&input, Some(0), Some(0), Some(2), &[]); - assert_eq_slice(&input, Some(1), Some(5), Some(2), &['b', 'd']); - - assert_eq_slice(&input, Some(0), Some(7), Some(3), &['a', 'd', 'g']); - assert_eq_slice(&input, Some(0), Some(6), Some(3), &['a', 'd']); - - assert_eq_slice(&input, Some(0), None, Some(10), &['a']); + assert_eq_slice( + &db, + &input, + Some(0), + Some(8), + Some(2), + &['a', 'c', 'e', 'g'], + ); + assert_eq_slice( + &db, + &input, + Some(0), + Some(7), + Some(2), + &['a', 'c', 'e', 'g'], + ); + assert_eq_slice(&db, &input, Some(0), Some(6), Some(2), &['a', 'c', 'e']); + assert_eq_slice(&db, &input, Some(0), Some(5), Some(2), &['a', 'c', 'e']); + assert_eq_slice(&db, &input, Some(0), Some(4), Some(2), &['a', 'c']); + assert_eq_slice(&db, &input, Some(0), Some(3), Some(2), &['a', 'c']); + assert_eq_slice(&db, &input, Some(0), Some(2), Some(2), &['a']); + assert_eq_slice(&db, &input, Some(0), Some(1), Some(2), &['a']); + assert_eq_slice(&db, &input, Some(0), Some(0), Some(2), &[]); + assert_eq_slice(&db, &input, Some(1), Some(5), Some(2), &['b', 'd']); + + assert_eq_slice(&db, &input, Some(0), Some(7), Some(3), &['a', 'd', 'g']); + assert_eq_slice(&db, &input, Some(0), Some(6), Some(3), &['a', 'd']); + + assert_eq_slice(&db, &input, Some(0), None, Some(10), &['a']); } #[test] fn py_slice_step_backward() { + let db = setup_db(); // indices: 0 1 2 3 4 5 6 let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; - assert_eq_slice(&input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']); - assert_eq_slice(&input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']); - assert_eq_slice(&input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']); - assert_eq_slice(&input, Some(4), Some(0), Some(-2), &['e', 'c']); - assert_eq_slice(&input, Some(3), Some(0), Some(-2), &['d', 'b']); - assert_eq_slice(&input, Some(2), Some(0), Some(-2), &['c']); - assert_eq_slice(&input, Some(1), Some(0), Some(-2), &['b']); - assert_eq_slice(&input, Some(0), Some(0), Some(-2), &[]); + assert_eq_slice(&db, &input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&db, &input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&db, &input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']); + assert_eq_slice(&db, &input, Some(4), Some(0), Some(-2), &['e', 'c']); + assert_eq_slice(&db, &input, Some(3), Some(0), Some(-2), &['d', 'b']); + assert_eq_slice(&db, &input, Some(2), Some(0), Some(-2), &['c']); + assert_eq_slice(&db, &input, Some(1), Some(0), Some(-2), &['b']); + assert_eq_slice(&db, &input, Some(0), Some(0), Some(-2), &[]); - assert_eq_slice(&input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']); - assert_eq_slice(&input, None, None, Some(-2), &['g', 'e', 'c', 'a']); - assert_eq_slice(&input, None, Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&db, &input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']); + assert_eq_slice(&db, &input, None, None, Some(-2), &['g', 'e', 'c', 'a']); + assert_eq_slice(&db, &input, None, Some(0), Some(-2), &['g', 'e', 'c']); - assert_eq_slice(&input, Some(5), Some(1), Some(-2), &['f', 'd']); - assert_eq_slice(&input, Some(5), Some(2), Some(-2), &['f', 'd']); - assert_eq_slice(&input, Some(5), Some(3), Some(-2), &['f']); - assert_eq_slice(&input, Some(5), Some(4), Some(-2), &['f']); - assert_eq_slice(&input, Some(5), Some(5), Some(-2), &[]); + assert_eq_slice(&db, &input, Some(5), Some(1), Some(-2), &['f', 'd']); + assert_eq_slice(&db, &input, Some(5), Some(2), Some(-2), &['f', 'd']); + assert_eq_slice(&db, &input, Some(5), Some(3), Some(-2), &['f']); + assert_eq_slice(&db, &input, Some(5), Some(4), Some(-2), &['f']); + assert_eq_slice(&db, &input, Some(5), Some(5), Some(-2), &[]); - assert_eq_slice(&input, Some(6), None, Some(-3), &['g', 'd', 'a']); - assert_eq_slice(&input, Some(6), Some(0), Some(-3), &['g', 'd']); + assert_eq_slice(&db, &input, Some(6), None, Some(-3), &['g', 'd', 'a']); + assert_eq_slice(&db, &input, Some(6), Some(0), Some(-3), &['g', 'd']); - assert_eq_slice(&input, Some(7), None, Some(-10), &['g']); + assert_eq_slice(&db, &input, Some(7), None, Some(-10), &['g']); - assert_eq_slice(&input, Some(-6), Some(-9), Some(-1), &['b', 'a']); - assert_eq_slice(&input, Some(-6), Some(-8), Some(-1), &['b', 'a']); - assert_eq_slice(&input, Some(-6), Some(-7), Some(-1), &['b']); - assert_eq_slice(&input, Some(-6), Some(-6), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-6), Some(-9), Some(-1), &['b', 'a']); + assert_eq_slice(&db, &input, Some(-6), Some(-8), Some(-1), &['b', 'a']); + assert_eq_slice(&db, &input, Some(-6), Some(-7), Some(-1), &['b']); + assert_eq_slice(&db, &input, Some(-6), Some(-6), Some(-1), &[]); - assert_eq_slice(&input, Some(-7), Some(-9), Some(-1), &['a']); + assert_eq_slice(&db, &input, Some(-7), Some(-9), Some(-1), &['a']); - assert_eq_slice(&input, Some(-8), Some(-9), Some(-1), &[]); - assert_eq_slice(&input, Some(-9), Some(-9), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-8), Some(-9), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-9), Some(-9), Some(-1), &[]); - assert_eq_slice(&input, Some(-6), Some(-2), Some(-1), &[]); - assert_eq_slice(&input, Some(-9), Some(-6), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-6), Some(-2), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-9), Some(-6), Some(-1), &[]); } }