mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-18 17:40:37 +00:00
[ty] Generate the top and bottom materialization of a type (#18594)
## Summary This is to support https://github.com/astral-sh/ruff/pull/18607. This PR adds support for generating the top materialization (or upper bound materialization) and the bottom materialization (or lower bound materialization) of a type. This is the most general and the most specific form of the type which is fully static, respectively. More concretely, `T'`, the top materialization of `T`, is the type `T` with all occurrences of dynamic type (`Any`, `Unknown`, `@Todo`) replaced as follows: - In covariant position, it's replaced with `object` - In contravariant position, it's replaced with `Never` - In invariant position, it's replaced with an unresolved type variable (For an invariant position, it should actually be replaced with an existential type, but this is not currently representable in our type system, so we use an unresolved type variable for now instead.) The bottom materialization is implemented in the same way, except we start out in "contravariant" position. ## Test Plan Add test cases for various types.
This commit is contained in:
parent
f74527f4e9
commit
ef4108af2a
12 changed files with 798 additions and 3 deletions
|
@ -0,0 +1,418 @@
|
|||
# Materialization
|
||||
|
||||
There are two materializations of a type:
|
||||
|
||||
- The top materialization (or upper bound materialization) of a type, which is the most general form
|
||||
of that type that is fully static
|
||||
- The bottom materialization (or lower bound materialization) of a type, which is the most specific
|
||||
form of that type that is fully static
|
||||
|
||||
More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of `Any` and
|
||||
`Unknown` replaced as follows:
|
||||
|
||||
- In covariant position, it's replaced with `object`
|
||||
- In contravariant position, it's replaced with `Never`
|
||||
- In invariant position, it's replaced with an unresolved type variable
|
||||
|
||||
The top materialization starts from the covariant position while the bottom materialization starts
|
||||
from the contravariant position.
|
||||
|
||||
TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an existential type
|
||||
representing "all lists, containing any type". We currently represent this by replacing `Any` in
|
||||
invariant position with an unresolved type variable.
|
||||
|
||||
## Replacement rules
|
||||
|
||||
### Top materialization
|
||||
|
||||
The dynamic type at the top-level is replaced with `object`.
|
||||
|
||||
```py
|
||||
from typing import Any, Callable
|
||||
from ty_extensions import Unknown, top_materialization
|
||||
|
||||
reveal_type(top_materialization(Any)) # revealed: object
|
||||
reveal_type(top_materialization(Unknown)) # revealed: object
|
||||
```
|
||||
|
||||
The contravariant position is replaced with `Never`.
|
||||
|
||||
```py
|
||||
reveal_type(top_materialization(Callable[[Any], None])) # revealed: (Never, /) -> None
|
||||
```
|
||||
|
||||
The invariant position is replaced with an unresolved type variable.
|
||||
|
||||
```py
|
||||
reveal_type(top_materialization(list[Any])) # revealed: list[T_all]
|
||||
```
|
||||
|
||||
### Bottom materialization
|
||||
|
||||
The dynamic type at the top-level is replaced with `Never`.
|
||||
|
||||
```py
|
||||
from typing import Any, Callable
|
||||
from ty_extensions import Unknown, bottom_materialization
|
||||
|
||||
reveal_type(bottom_materialization(Any)) # revealed: Never
|
||||
reveal_type(bottom_materialization(Unknown)) # revealed: Never
|
||||
```
|
||||
|
||||
The contravariant position is replaced with `object`.
|
||||
|
||||
```py
|
||||
# revealed: (object, object, /) -> None
|
||||
reveal_type(bottom_materialization(Callable[[Any, Unknown], None]))
|
||||
```
|
||||
|
||||
The invariant position is replaced in the same way as the top materialization, with an unresolved
|
||||
type variable.
|
||||
|
||||
```py
|
||||
reveal_type(bottom_materialization(list[Any])) # revealed: list[T_all]
|
||||
```
|
||||
|
||||
## Fully static types
|
||||
|
||||
The top / bottom (and only) materialization of any fully static type is just itself.
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
from ty_extensions import TypeOf, bottom_materialization, top_materialization
|
||||
|
||||
reveal_type(top_materialization(int)) # revealed: int
|
||||
reveal_type(bottom_materialization(int)) # revealed: int
|
||||
|
||||
reveal_type(top_materialization(Literal[1])) # revealed: Literal[1]
|
||||
reveal_type(bottom_materialization(Literal[1])) # revealed: Literal[1]
|
||||
|
||||
reveal_type(top_materialization(Literal[True])) # revealed: Literal[True]
|
||||
reveal_type(bottom_materialization(Literal[True])) # revealed: Literal[True]
|
||||
|
||||
reveal_type(top_materialization(Literal["abc"])) # revealed: Literal["abc"]
|
||||
reveal_type(bottom_materialization(Literal["abc"])) # revealed: Literal["abc"]
|
||||
|
||||
reveal_type(top_materialization(int | str)) # revealed: int | str
|
||||
reveal_type(bottom_materialization(int | str)) # revealed: int | str
|
||||
```
|
||||
|
||||
We currently treat function literals as fully static types, so they remain unchanged even though the
|
||||
signature might have `Any` in it. (TODO: this is probably not right.)
|
||||
|
||||
```py
|
||||
def function(x: Any) -> None: ...
|
||||
|
||||
class A:
|
||||
def method(self, x: Any) -> None: ...
|
||||
|
||||
reveal_type(top_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None
|
||||
|
||||
reveal_type(top_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
For a callable, the parameter types are in a contravariant position, and the return type is in a
|
||||
covariant position.
|
||||
|
||||
```py
|
||||
from typing import Any, Callable
|
||||
from ty_extensions import TypeOf, Unknown, bottom_materialization, top_materialization
|
||||
|
||||
def _(callable: Callable[[Any, Unknown], Any]) -> None:
|
||||
# revealed: (Never, Never, /) -> object
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (object, object, /) -> Never
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
```
|
||||
|
||||
The parameter types in a callable inherits the contravariant position.
|
||||
|
||||
```py
|
||||
def _(callable: Callable[[int, tuple[int | Any]], tuple[Any]]) -> None:
|
||||
# revealed: (int, tuple[int], /) -> tuple[object]
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (int, tuple[object], /) -> Never
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
```
|
||||
|
||||
But, if the callable itself is in a contravariant position, then the variance is flipped i.e., if
|
||||
the outer variance is covariant, it's flipped to contravariant, and if it's contravariant, it's
|
||||
flipped to covariant, invariant remains invariant.
|
||||
|
||||
```py
|
||||
def _(callable: Callable[[Any, Callable[[Unknown], Any]], Callable[[Any, int], Any]]) -> None:
|
||||
# revealed: (Never, (object, /) -> Never, /) -> (Never, int, /) -> object
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (object, (Never, /) -> object, /) -> (object, int, /) -> Never
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
```
|
||||
|
||||
## Tuple
|
||||
|
||||
All positions in a tuple are covariant.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Unknown, bottom_materialization, top_materialization
|
||||
|
||||
reveal_type(top_materialization(tuple[Any, int])) # revealed: tuple[object, int]
|
||||
reveal_type(bottom_materialization(tuple[Any, int])) # revealed: Never
|
||||
|
||||
reveal_type(top_materialization(tuple[Unknown, int])) # revealed: tuple[object, int]
|
||||
reveal_type(bottom_materialization(tuple[Unknown, int])) # revealed: Never
|
||||
|
||||
reveal_type(top_materialization(tuple[Any, int, Unknown])) # revealed: tuple[object, int, object]
|
||||
reveal_type(bottom_materialization(tuple[Any, int, Unknown])) # revealed: Never
|
||||
```
|
||||
|
||||
Except for when the tuple itself is in a contravariant position, then all positions in the tuple
|
||||
inherit the contravariant position.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from ty_extensions import TypeOf
|
||||
|
||||
def _(callable: Callable[[tuple[Any, int], tuple[str, Unknown]], None]) -> None:
|
||||
# revealed: (Never, Never, /) -> None
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (tuple[object, int], tuple[str, object], /) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
```
|
||||
|
||||
And, similarly for an invariant position.
|
||||
|
||||
```py
|
||||
reveal_type(top_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]]
|
||||
reveal_type(bottom_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]]
|
||||
|
||||
reveal_type(top_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]]
|
||||
reveal_type(bottom_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]]
|
||||
|
||||
reveal_type(top_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]]
|
||||
reveal_type(bottom_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]]
|
||||
```
|
||||
|
||||
## Union
|
||||
|
||||
All positions in a union are covariant.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Unknown, bottom_materialization, top_materialization
|
||||
|
||||
reveal_type(top_materialization(Any | int)) # revealed: object
|
||||
reveal_type(bottom_materialization(Any | int)) # revealed: int
|
||||
|
||||
reveal_type(top_materialization(Unknown | int)) # revealed: object
|
||||
reveal_type(bottom_materialization(Unknown | int)) # revealed: int
|
||||
|
||||
reveal_type(top_materialization(int | str | Any)) # revealed: object
|
||||
reveal_type(bottom_materialization(int | str | Any)) # revealed: int | str
|
||||
```
|
||||
|
||||
Except for when the union itself is in a contravariant position, then all positions in the union
|
||||
inherit the contravariant position.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from ty_extensions import TypeOf
|
||||
|
||||
def _(callable: Callable[[Any | int, str | Unknown], None]) -> None:
|
||||
# revealed: (int, str, /) -> None
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (object, object, /) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
```
|
||||
|
||||
And, similarly for an invariant position.
|
||||
|
||||
```py
|
||||
reveal_type(top_materialization(list[Any | int])) # revealed: list[T_all | int]
|
||||
reveal_type(bottom_materialization(list[Any | int])) # revealed: list[T_all | int]
|
||||
|
||||
reveal_type(top_materialization(list[str | Unknown])) # revealed: list[str | T_all]
|
||||
reveal_type(bottom_materialization(list[str | Unknown])) # revealed: list[str | T_all]
|
||||
|
||||
reveal_type(top_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int]
|
||||
reveal_type(bottom_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int]
|
||||
```
|
||||
|
||||
## Intersection
|
||||
|
||||
All positions in an intersection are covariant.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection, Unknown, bottom_materialization, top_materialization
|
||||
|
||||
reveal_type(top_materialization(Intersection[Any, int])) # revealed: int
|
||||
reveal_type(bottom_materialization(Intersection[Any, int])) # revealed: Never
|
||||
|
||||
# Here, the top materialization of `Any | int` is `object` and the intersection of it with tuple
|
||||
# revealed: tuple[str, object]
|
||||
reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
||||
# revealed: Never
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
||||
|
||||
# revealed: int & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str]]))
|
||||
|
||||
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
```
|
||||
|
||||
## Negation (via `Not`)
|
||||
|
||||
All positions in a negation are contravariant.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Not, Unknown, bottom_materialization, top_materialization
|
||||
|
||||
# ~Any is still Any, so the top materialization is object
|
||||
reveal_type(top_materialization(Not[Any])) # revealed: object
|
||||
reveal_type(bottom_materialization(Not[Any])) # revealed: Never
|
||||
|
||||
# tuple[Any, int] is in a contravariant position, so the
|
||||
# top materialization is Never and the negation of it
|
||||
# revealed: object
|
||||
reveal_type(top_materialization(Not[tuple[Any, int]]))
|
||||
# revealed: ~tuple[object, int]
|
||||
reveal_type(bottom_materialization(Not[tuple[Any, int]]))
|
||||
```
|
||||
|
||||
## `type`
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Unknown, bottom_materialization, top_materialization
|
||||
|
||||
reveal_type(top_materialization(type[Any])) # revealed: type
|
||||
reveal_type(bottom_materialization(type[Any])) # revealed: Never
|
||||
|
||||
reveal_type(top_materialization(type[Unknown])) # revealed: type
|
||||
reveal_type(bottom_materialization(type[Unknown])) # revealed: Never
|
||||
|
||||
reveal_type(top_materialization(type[int | Any])) # revealed: type
|
||||
reveal_type(bottom_materialization(type[int | Any])) # revealed: type[int]
|
||||
|
||||
# Here, `T` has an upper bound of `type`
|
||||
reveal_type(top_materialization(list[type[Any]])) # revealed: list[T_all]
|
||||
reveal_type(bottom_materialization(list[type[Any]])) # revealed: list[T_all]
|
||||
```
|
||||
|
||||
## Type variables
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any, Never, TypeVar
|
||||
from ty_extensions import (
|
||||
TypeOf,
|
||||
Unknown,
|
||||
bottom_materialization,
|
||||
top_materialization,
|
||||
is_fully_static,
|
||||
static_assert,
|
||||
is_subtype_of,
|
||||
)
|
||||
|
||||
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
# Top materialization of `T: Any` is `T: object`
|
||||
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
|
||||
|
||||
# Bottom materialization of `T: Any` is `T: Never`
|
||||
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
|
||||
# TODO: This should not error, see https://github.com/astral-sh/ty/issues/638
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never))
|
||||
|
||||
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
# Top materialization of `T: (int, Any)` is `T: (int, object)`
|
||||
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
|
||||
|
||||
# Bottom materialization of `T: (int, Any)` is `T: (int, Never)`
|
||||
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
|
||||
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int))
|
||||
```
|
||||
|
||||
## Generics
|
||||
|
||||
For generics, the materialization depends on the surrounding variance and the variance of the type
|
||||
variable itself.
|
||||
|
||||
- If the type variable is invariant, the materialization happens in an invariant position
|
||||
- If the type variable is covariant, the materialization happens as per the surrounding variance
|
||||
- If the type variable is contravariant, the materialization happens as per the surrounding
|
||||
variance, but the variance is flipped
|
||||
|
||||
```py
|
||||
from typing import Any, Generic, TypeVar
|
||||
from ty_extensions import bottom_materialization, top_materialization
|
||||
|
||||
T = TypeVar("T")
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
T_contra = TypeVar("T_contra", contravariant=True)
|
||||
|
||||
class GenericInvariant(Generic[T]):
|
||||
pass
|
||||
|
||||
class GenericCovariant(Generic[T_co]):
|
||||
pass
|
||||
|
||||
class GenericContravariant(Generic[T_contra]):
|
||||
pass
|
||||
|
||||
reveal_type(top_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all]
|
||||
reveal_type(bottom_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all]
|
||||
|
||||
reveal_type(top_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[object]
|
||||
reveal_type(bottom_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[Never]
|
||||
|
||||
reveal_type(top_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[Never]
|
||||
reveal_type(bottom_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[object]
|
||||
```
|
||||
|
||||
Parameters in callable are contravariant, so the variance should be flipped:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from ty_extensions import TypeOf
|
||||
|
||||
def invariant(callable: Callable[[GenericInvariant[Any]], None]) -> None:
|
||||
# revealed: (GenericInvariant[T_all], /) -> None
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (GenericInvariant[T_all], /) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
|
||||
def covariant(callable: Callable[[GenericCovariant[Any]], None]) -> None:
|
||||
# revealed: (GenericCovariant[Never], /) -> None
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (GenericCovariant[object], /) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
|
||||
def contravariant(callable: Callable[[GenericContravariant[Any]], None]) -> None:
|
||||
# revealed: (GenericContravariant[object], /) -> None
|
||||
reveal_type(top_materialization(TypeOf[callable]))
|
||||
|
||||
# revealed: (GenericContravariant[Never], /) -> None
|
||||
reveal_type(bottom_materialization(TypeOf[callable]))
|
||||
```
|
|
@ -615,6 +615,120 @@ impl<'db> Type<'db> {
|
|||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
/// Returns the top materialization (or upper bound materialization) of this type, which is the
|
||||
/// most general form of the type that is fully static.
|
||||
#[must_use]
|
||||
pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.materialize(db, TypeVarVariance::Covariant)
|
||||
}
|
||||
|
||||
/// Returns the bottom materialization (or lower bound materialization) of this type, which is
|
||||
/// the most specific form of the type that is fully static.
|
||||
#[must_use]
|
||||
pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.materialize(db, TypeVarVariance::Contravariant)
|
||||
}
|
||||
|
||||
/// Returns the materialization of this type depending on the given `variance`.
|
||||
///
|
||||
/// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of
|
||||
/// the dynamic types (`Any`, `Unknown`, `Todo`) replaced as follows:
|
||||
///
|
||||
/// - In covariant position, it's replaced with `object`
|
||||
/// - In contravariant position, it's replaced with `Never`
|
||||
/// - In invariant position, it's replaced with an unresolved type variable
|
||||
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
|
||||
match self {
|
||||
Type::Dynamic(_) => match variance {
|
||||
// TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an
|
||||
// existential type representing "all lists, containing any type." We currently
|
||||
// represent this by replacing `Any` in invariant position with an unresolved type
|
||||
// variable.
|
||||
TypeVarVariance::Invariant => Type::TypeVar(TypeVarInstance::new(
|
||||
db,
|
||||
Name::new_static("T_all"),
|
||||
None,
|
||||
None,
|
||||
variance,
|
||||
None,
|
||||
TypeVarKind::Pep695,
|
||||
)),
|
||||
TypeVarVariance::Covariant => Type::object(db),
|
||||
TypeVarVariance::Contravariant => Type::Never,
|
||||
TypeVarVariance::Bivariant => unreachable!(),
|
||||
},
|
||||
|
||||
Type::Never
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::BoundSuper(_) => *self,
|
||||
|
||||
Type::FunctionLiteral(_) | Type::BoundMethod(_) => {
|
||||
// TODO: Subtyping between function / methods with a callable accounts for the
|
||||
// signature (parameters and return type), so we might need to do something here
|
||||
*self
|
||||
}
|
||||
|
||||
Type::NominalInstance(nominal_instance_type) => {
|
||||
Type::NominalInstance(nominal_instance_type.materialize(db, variance))
|
||||
}
|
||||
Type::GenericAlias(generic_alias) => {
|
||||
Type::GenericAlias(generic_alias.materialize(db, variance))
|
||||
}
|
||||
Type::Callable(callable_type) => {
|
||||
Type::Callable(callable_type.materialize(db, variance))
|
||||
}
|
||||
Type::SubclassOf(subclass_of_type) => subclass_of_type.materialize(db, variance),
|
||||
Type::ProtocolInstance(protocol_instance_type) => {
|
||||
// TODO: Add tests for this once subtyping/assignability is implemented for
|
||||
// protocols. It _might_ require changing the logic here because:
|
||||
//
|
||||
// > Subtyping for protocol instances involves taking account of the fact that
|
||||
// > read-only property members, and method members, on protocols act covariantly;
|
||||
// > write-only property members act contravariantly; and read/write attribute
|
||||
// > members on protocols act invariantly
|
||||
Type::ProtocolInstance(protocol_instance_type.materialize(db, variance))
|
||||
}
|
||||
Type::Union(union_type) => union_type.map(db, |ty| ty.materialize(db, variance)),
|
||||
Type::Intersection(intersection_type) => IntersectionBuilder::new(db)
|
||||
.positive_elements(
|
||||
intersection_type
|
||||
.positive(db)
|
||||
.iter()
|
||||
.map(|ty| ty.materialize(db, variance)),
|
||||
)
|
||||
.negative_elements(
|
||||
intersection_type
|
||||
.negative(db)
|
||||
.iter()
|
||||
.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::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace references to the class `class` with a self-reference marker. This is currently
|
||||
/// used for recursive protocols, but could probably be extended to self-referential type-
|
||||
/// aliases and similar.
|
||||
|
@ -3634,6 +3748,21 @@ impl<'db> Type<'db> {
|
|||
)
|
||||
.into(),
|
||||
|
||||
Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => {
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static(
|
||||
"type",
|
||||
)))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any())]),
|
||||
Some(Type::any()),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
Some(KnownFunction::AssertType) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
|
@ -5984,6 +6113,19 @@ impl<'db> TypeVarInstance<'db> {
|
|||
self.kind(db),
|
||||
)
|
||||
}
|
||||
|
||||
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db),
|
||||
self.definition(db),
|
||||
self.bound_or_constraints(db)
|
||||
.map(|b| b.materialize(db, variance)),
|
||||
self.variance(db),
|
||||
self.default_ty(db),
|
||||
self.kind(db),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
|
@ -5994,6 +6136,20 @@ pub enum TypeVarVariance {
|
|||
Bivariant,
|
||||
}
|
||||
|
||||
impl TypeVarVariance {
|
||||
/// Flips the polarity of the variance.
|
||||
///
|
||||
/// Covariant becomes contravariant, contravariant becomes covariant, others remain unchanged.
|
||||
pub(crate) const fn flip(self) -> Self {
|
||||
match self {
|
||||
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
|
||||
TypeVarVariance::Covariant => TypeVarVariance::Contravariant,
|
||||
TypeVarVariance::Contravariant => TypeVarVariance::Covariant,
|
||||
TypeVarVariance::Bivariant => TypeVarVariance::Bivariant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum TypeVarBoundOrConstraints<'db> {
|
||||
UpperBound(Type<'db>),
|
||||
|
@ -6011,6 +6167,25 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
match self {
|
||||
TypeVarBoundOrConstraints::UpperBound(bound) => {
|
||||
TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, variance))
|
||||
}
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
TypeVarBoundOrConstraints::Constraints(UnionType::new(
|
||||
db,
|
||||
constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.materialize(db, variance))
|
||||
.collect::<Vec<_>>()
|
||||
.into_boxed_slice(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned if a type is not (or may not be) a context manager.
|
||||
|
@ -7012,6 +7187,14 @@ impl<'db> CallableType<'db> {
|
|||
))
|
||||
}
|
||||
|
||||
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
CallableType::new(
|
||||
db,
|
||||
self.signatures(db).materialize(db, variance),
|
||||
self.is_function_like(db),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a callable type which represents a fully-static "bottom" callable.
|
||||
///
|
||||
/// Specifically, this represents a callable type with a single signature:
|
||||
|
|
|
@ -675,6 +675,18 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::TopMaterialization) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(ty.top_materialization(db));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::BottomMaterialization) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(ty.bottom_materialization(db));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
if let [Some(first_arg)] = overload.parameter_types() {
|
||||
if let Some(len_ty) = first_arg.len(db) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::hash::BuildHasherDefault;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use super::TypeVarVariance;
|
||||
use super::{
|
||||
IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType,
|
||||
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, infer_expression_type,
|
||||
|
@ -173,6 +174,14 @@ impl<'db> GenericAlias<'db> {
|
|||
Self::new(db, self.origin(db), self.specialization(db).normalized(db))
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.origin(db),
|
||||
self.specialization(db).materialize(db, variance),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
self.origin(db).definition(db)
|
||||
}
|
||||
|
@ -223,6 +232,13 @@ impl<'db> ClassType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
match self {
|
||||
Self::NonGeneric(_) => self,
|
||||
Self::Generic(generic) => Self::Generic(generic.materialize(db, variance)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Self::NonGeneric(class) => class.has_pep_695_type_params(db),
|
||||
|
|
|
@ -890,6 +890,10 @@ pub enum KnownFunction {
|
|||
DunderAllNames,
|
||||
/// `ty_extensions.all_members`
|
||||
AllMembers,
|
||||
/// `ty_extensions.top_materialization`
|
||||
TopMaterialization,
|
||||
/// `ty_extensions.bottom_materialization`
|
||||
BottomMaterialization,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
|
@ -947,6 +951,8 @@ impl KnownFunction {
|
|||
| Self::IsSingleValued
|
||||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
| Self::TopMaterialization
|
||||
| Self::BottomMaterialization
|
||||
| Self::GenericContext
|
||||
| Self::DunderAllNames
|
||||
| Self::StaticAssert
|
||||
|
@ -1007,6 +1013,8 @@ pub(crate) mod tests {
|
|||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsGradualEquivalentTo
|
||||
| KnownFunction::TopMaterialization
|
||||
| KnownFunction::BottomMaterialization
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
};
|
||||
|
||||
|
|
|
@ -358,6 +358,25 @@ impl<'db> Specialization<'db> {
|
|||
Self::new(db, self.generic_context(db), types)
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
let types: Box<[_]> = self
|
||||
.generic_context(db)
|
||||
.variables(db)
|
||||
.into_iter()
|
||||
.zip(self.types(db))
|
||||
.map(|(typevar, vartype)| {
|
||||
let variance = match typevar.variance(db) {
|
||||
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
|
||||
TypeVarVariance::Covariant => variance,
|
||||
TypeVarVariance::Contravariant => variance.flip(),
|
||||
TypeVarVariance::Bivariant => unreachable!(),
|
||||
};
|
||||
vartype.materialize(db, variance)
|
||||
})
|
||||
.collect();
|
||||
Specialization::new(db, self.generic_context(db), types)
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use super::protocol_class::ProtocolInterface;
|
||||
use super::{ClassType, KnownClass, SubclassOfType, Type};
|
||||
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
|
||||
use crate::place::{Boundness, Place, PlaceAndQualifiers};
|
||||
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
@ -80,6 +80,10 @@ impl<'db> NominalInstanceType<'db> {
|
|||
Self::from_class(self.class.normalized(db))
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self::from_class(self.class.materialize(db, variance))
|
||||
}
|
||||
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
@ -314,6 +318,16 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
match self.inner {
|
||||
// TODO: This should also materialize via `class.materialize(db, variance)`
|
||||
Protocol::FromClass(class) => Self::from_class(class),
|
||||
Protocol::Synthesized(synthesized) => {
|
||||
Self::synthesized(synthesized.materialize(db, variance))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
@ -370,7 +384,7 @@ impl<'db> Protocol<'db> {
|
|||
|
||||
mod synthesized_protocol {
|
||||
use crate::types::protocol_class::ProtocolInterface;
|
||||
use crate::types::{TypeMapping, TypeVarInstance};
|
||||
use crate::types::{TypeMapping, TypeVarInstance, TypeVarVariance};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
/// A "synthesized" protocol type that is dissociated from a class definition in source code.
|
||||
|
@ -390,6 +404,10 @@ mod synthesized_protocol {
|
|||
Self(interface.normalized(db))
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self(self.0.materialize(db, variance))
|
||||
}
|
||||
|
||||
pub(super) fn apply_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -303,4 +303,20 @@ mod flaky {
|
|||
negation_reverses_subtype_order, db,
|
||||
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
|
||||
);
|
||||
|
||||
// Both the top and bottom materialization tests are flaky in part due to various failures that
|
||||
// it discovers in the current implementation of assignability of the types.
|
||||
// TODO: Create a issue with some example failures to keep track of it
|
||||
|
||||
// `T'`, the top materialization of `T`, should be assignable to `T`.
|
||||
type_property_test!(
|
||||
top_materialization_of_type_is_assignable_to_type, db,
|
||||
forall types t. t.top_materialization(db).is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// Similarly, `T'`, the bottom materialization of `T`, should also be assignable to `T`.
|
||||
type_property_test!(
|
||||
bottom_materialization_of_type_is_assigneble_to_type, db,
|
||||
forall types t. t.bottom_materialization(db).is_assignable_to(db, t)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ use crate::{
|
|||
{Db, FxOrderSet},
|
||||
};
|
||||
|
||||
use super::TypeVarVariance;
|
||||
|
||||
impl<'db> ClassLiteral<'db> {
|
||||
/// Returns `Some` if this is a protocol class, `None` otherwise.
|
||||
pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option<ProtocolClassLiteral<'db>> {
|
||||
|
@ -177,6 +179,28 @@ impl<'db> ProtocolInterface<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
match self {
|
||||
Self::Members(members) => Self::Members(ProtocolInterfaceMembers::new(
|
||||
db,
|
||||
members
|
||||
.inner(db)
|
||||
.iter()
|
||||
.map(|(name, data)| {
|
||||
(
|
||||
name.clone(),
|
||||
ProtocolMemberData {
|
||||
ty: data.ty.materialize(db, variance),
|
||||
qualifiers: data.qualifiers,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
)),
|
||||
Self::SelfReference => Self::SelfReference,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn specialized_and_normalized<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -15,7 +15,7 @@ use std::{collections::HashMap, slice::Iter};
|
|||
use itertools::EitherOrBoth;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
|
||||
use super::{DynamicType, Type, definition_expression_type};
|
||||
use super::{DynamicType, Type, TypeVarVariance, definition_expression_type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeRelation, TypeVarInstance, todo_type};
|
||||
|
@ -53,6 +53,14 @@ impl<'db> CallableSignature<'db> {
|
|||
self.overloads.iter()
|
||||
}
|
||||
|
||||
pub(super) fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self::from_overloads(
|
||||
self.overloads
|
||||
.iter()
|
||||
.map(|signature| signature.materialize(db, variance)),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
|
||||
Self::from_overloads(
|
||||
self.overloads
|
||||
|
@ -353,6 +361,20 @@ impl<'db> Signature<'db> {
|
|||
Self::new(Parameters::object(db), Some(Type::Never))
|
||||
}
|
||||
|
||||
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self {
|
||||
generic_context: self.generic_context,
|
||||
inherited_generic_context: self.inherited_generic_context,
|
||||
// Parameters are at contravariant position, so the variance is flipped.
|
||||
parameters: self.parameters.materialize(db, variance.flip()),
|
||||
return_ty: Some(
|
||||
self.return_ty
|
||||
.unwrap_or(Type::unknown())
|
||||
.materialize(db, variance),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
generic_context: self.generic_context.map(|ctx| ctx.normalized(db)),
|
||||
|
@ -984,6 +1006,17 @@ impl<'db> Parameters<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
if self.is_gradual {
|
||||
Parameters::object(db)
|
||||
} else {
|
||||
Parameters::new(
|
||||
self.iter()
|
||||
.map(|parameter| parameter.materialize(db, variance)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_slice(&self) -> &[Parameter<'db>] {
|
||||
self.value.as_slice()
|
||||
}
|
||||
|
@ -1304,6 +1337,18 @@ impl<'db> Parameter<'db> {
|
|||
self
|
||||
}
|
||||
|
||||
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
Self {
|
||||
annotated_type: Some(
|
||||
self.annotated_type
|
||||
.unwrap_or(Type::unknown())
|
||||
.materialize(db, variance),
|
||||
),
|
||||
kind: self.kind.clone(),
|
||||
form: self.form,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
|
||||
Self {
|
||||
annotated_type: self
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use ruff_python_ast::name::Name;
|
||||
|
||||
use crate::place::PlaceAndQualifiers;
|
||||
use crate::types::{
|
||||
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation,
|
||||
|
@ -5,6 +7,8 @@ use crate::types::{
|
|||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
use super::{TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance};
|
||||
|
||||
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub struct SubclassOfType<'db> {
|
||||
|
@ -73,6 +77,32 @@ impl<'db> SubclassOfType<'db> {
|
|||
!self.is_dynamic()
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
|
||||
match self.subclass_of {
|
||||
SubclassOfInner::Dynamic(_) => match variance {
|
||||
TypeVarVariance::Covariant => KnownClass::Type.to_instance(db),
|
||||
TypeVarVariance::Contravariant => Type::Never,
|
||||
TypeVarVariance::Invariant => {
|
||||
// We need to materialize this to `type[T]` but that isn't representable so
|
||||
// we instead use a type variable with an upper bound of `type`.
|
||||
Type::TypeVar(TypeVarInstance::new(
|
||||
db,
|
||||
Name::new_static("T_all"),
|
||||
None,
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(
|
||||
KnownClass::Type.to_instance(db),
|
||||
)),
|
||||
variance,
|
||||
None,
|
||||
TypeVarKind::Pep695,
|
||||
))
|
||||
}
|
||||
TypeVarVariance::Bivariant => unreachable!(),
|
||||
},
|
||||
SubclassOfInner::Class(_) => Type::SubclassOf(self),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -44,6 +44,12 @@ def generic_context(type: Any) -> Any: ...
|
|||
# either the module does not have `__all__` or it has invalid elements.
|
||||
def dunder_all_names(module: Any) -> Any: ...
|
||||
|
||||
# Returns the type that's an upper bound of materializing the given (gradual) type.
|
||||
def top_materialization(type: Any) -> Any: ...
|
||||
|
||||
# Returns the type that's a lower bound of materializing the given (gradual) type.
|
||||
def bottom_materialization(type: Any) -> Any: ...
|
||||
|
||||
# Returns a tuple of all members of the given object, similar to `dir(obj)` and
|
||||
# `inspect.getmembers(obj)`, with at least the following differences:
|
||||
#
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue