diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md new file mode 100644 index 0000000000..638ad5ff20 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -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])) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 80c7bd569b..94dbe1be27 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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::>() + .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: diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a7b6e9d94b..45f7d5694d 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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) { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index bf5e6f494a..78532e12de 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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), diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 144a508b03..ae693bf731 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -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, }; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index a4ebf02a69..56c4df3426 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -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, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 40ba5d6719..715fe51c6d 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -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, diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index e0bca1c509..de97903296 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -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) + ); } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b17864490e..df3a633367 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -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> { @@ -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::>(), + )), + Self::SelfReference => Self::SelfReference, + } + } + pub(super) fn specialized_and_normalized<'a>( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 0e17986b1e..810a5a8a8e 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -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 diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 2143d2e1e2..244945b5ca 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -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, diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 0b98b7a55e..81838fdacc 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -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: #