diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 2190fd8796..65eeea44a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -47,7 +47,7 @@ The invariant position is replaced with an unresolved type variable. ```py def _(top_list: Top[list[Any]]): - reveal_type(top_list) # revealed: list[T_all] + reveal_type(top_list) # revealed: Top[list[Any]] ``` ### Bottom materialization @@ -75,7 +75,7 @@ type variable. ```py def _(bottom_list: Bottom[list[Any]]): - reveal_type(bottom_list) # revealed: list[T_all] + reveal_type(bottom_list) # revealed: Bottom[list[Any]] ``` ## Fully static types @@ -230,14 +230,14 @@ def _( top_aiu: Top[LTAnyIntUnknown], bottom_aiu: Bottom[LTAnyIntUnknown], ): - reveal_type(top_ai) # revealed: list[tuple[T_all, int]] - reveal_type(bottom_ai) # revealed: list[tuple[T_all, int]] + reveal_type(top_ai) # revealed: Top[list[tuple[Any, int]]] + reveal_type(bottom_ai) # revealed: Bottom[list[tuple[Any, int]]] - reveal_type(top_su) # revealed: list[tuple[str, T_all]] - reveal_type(bottom_su) # revealed: list[tuple[str, T_all]] + reveal_type(top_su) # revealed: Top[list[tuple[str, Unknown]]] + reveal_type(bottom_su) # revealed: Bottom[list[tuple[str, Unknown]]] - reveal_type(top_aiu) # revealed: list[tuple[T_all, int, T_all]] - reveal_type(bottom_aiu) # revealed: list[tuple[T_all, int, T_all]] + reveal_type(top_aiu) # revealed: Top[list[tuple[Any, int, Unknown]]] + reveal_type(bottom_aiu) # revealed: Bottom[list[tuple[Any, int, Unknown]]] ``` ## Union @@ -286,14 +286,14 @@ def _( top_aiu: Top[list[Any | int | Unknown]], bottom_aiu: Bottom[list[Any | int | Unknown]], ): - reveal_type(top_ai) # revealed: list[T_all | int] - reveal_type(bottom_ai) # revealed: list[T_all | int] + reveal_type(top_ai) # revealed: Top[list[Any | int]] + reveal_type(bottom_ai) # revealed: Bottom[list[Any | int]] - reveal_type(top_su) # revealed: list[str | T_all] - reveal_type(bottom_su) # revealed: list[str | T_all] + reveal_type(top_su) # revealed: Top[list[str | Unknown]] + reveal_type(bottom_su) # revealed: Bottom[list[str | Unknown]] - reveal_type(top_aiu) # revealed: list[T_all | int] - reveal_type(bottom_aiu) # revealed: list[T_all | int] + reveal_type(top_aiu) # revealed: Top[list[Any | int]] + reveal_type(bottom_aiu) # revealed: Bottom[list[Any | int]] ``` ## Intersection @@ -320,8 +320,10 @@ def _( top: Top[Intersection[list[Any], list[int]]], bottom: Bottom[Intersection[list[Any], list[int]]], ): - reveal_type(top) # revealed: list[T_all] & list[int] - reveal_type(bottom) # revealed: list[T_all] & list[int] + # Top[list[Any] & list[int]] = Top[list[Any]] & list[int] = list[int] + reveal_type(top) # revealed: list[int] + # Bottom[list[Any] & list[int]] = Bottom[list[Any]] & list[int] = Bottom[list[Any]] + reveal_type(bottom) # revealed: Bottom[list[Any]] ``` ## Negation (via `Not`) @@ -366,8 +368,8 @@ static_assert(is_equivalent_to(Bottom[type[int | Any]], type[int])) # Here, `T` has an upper bound of `type` def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]): - reveal_type(top) # revealed: list[T_all] - reveal_type(bottom) # revealed: list[T_all] + reveal_type(top) # revealed: Top[list[type[Any]]] + reveal_type(bottom) # revealed: Bottom[list[type[Any]]] ``` ## Type variables @@ -427,8 +429,8 @@ class GenericContravariant(Generic[T_contra]): pass def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]): - reveal_type(top) # revealed: GenericInvariant[T_all] - reveal_type(bottom) # revealed: GenericInvariant[T_all] + reveal_type(top) # revealed: Top[GenericInvariant[Any]] + reveal_type(bottom) # revealed: Bottom[GenericInvariant[Any]] static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object])) static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never])) @@ -448,8 +450,8 @@ type CovariantCallable = Callable[[GenericCovariant[Any]], None] type ContravariantCallable = Callable[[GenericContravariant[Any]], None] def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None: - reveal_type(top) # revealed: (GenericInvariant[T_all], /) -> None - reveal_type(bottom) # revealed: (GenericInvariant[T_all], /) -> None + reveal_type(top) # revealed: (Bottom[GenericInvariant[Any]], /) -> None + reveal_type(bottom) # revealed: (Top[GenericInvariant[Any]], /) -> None def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None: reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None @@ -492,3 +494,207 @@ def _( bottom_1: Bottom[1], # error: [invalid-type-form] ): ... ``` + +## Nested use + +`Top[T]` and `Bottom[T]` are always fully static types. Therefore, they have only one +materialization (themselves) and applying `Top` or `Bottom` again does nothing. + +```py +from typing import Any +from ty_extensions import Top, Bottom, static_assert, is_equivalent_to + +static_assert(is_equivalent_to(Top[Top[list[Any]]], Top[list[Any]])) +static_assert(is_equivalent_to(Bottom[Top[list[Any]]], Top[list[Any]])) + +static_assert(is_equivalent_to(Bottom[Bottom[list[Any]]], Bottom[list[Any]])) +static_assert(is_equivalent_to(Top[Bottom[list[Any]]], Bottom[list[Any]])) +``` + +## Subtyping + +Any `list[T]` is a subtype of `Top[list[Any]]`, but with more restrictive gradual types, not all +other specializations are subtypes. + +```py +from typing import Any, Literal +from ty_extensions import is_subtype_of, static_assert, Top, Intersection, Bottom + +# None and Top +static_assert(is_subtype_of(list[int], Top[list[Any]])) +static_assert(not is_subtype_of(Top[list[Any]], list[int])) +static_assert(is_subtype_of(list[bool], Top[list[Intersection[int, Any]]])) +static_assert(is_subtype_of(list[int], Top[list[Intersection[int, Any]]])) +static_assert(not is_subtype_of(list[int | str], Top[list[Intersection[int, Any]]])) +static_assert(not is_subtype_of(list[object], Top[list[Intersection[int, Any]]])) +static_assert(not is_subtype_of(list[str], Top[list[Intersection[int, Any]]])) +static_assert(not is_subtype_of(list[str | bool], Top[list[Intersection[int, Any]]])) + +# Top and Top +static_assert(is_subtype_of(Top[list[int | Any]], Top[list[Any]])) +static_assert(not is_subtype_of(Top[list[Any]], Top[list[int | Any]])) +static_assert(is_subtype_of(Top[list[Intersection[int, Any]]], Top[list[Any]])) +static_assert(not is_subtype_of(Top[list[Any]], Top[list[Intersection[int, Any]]])) +static_assert(not is_subtype_of(Top[list[Intersection[int, Any]]], Top[list[int | Any]])) +static_assert(not is_subtype_of(Top[list[int | Any]], Top[list[Intersection[int, Any]]])) +static_assert(not is_subtype_of(Top[list[str | Any]], Top[list[int | Any]])) +static_assert(is_subtype_of(Top[list[str | int | Any]], Top[list[int | Any]])) +static_assert(not is_subtype_of(Top[list[int | Any]], Top[list[str | int | Any]])) + +# Bottom and Top +static_assert(is_subtype_of(Bottom[list[Any]], Top[list[Any]])) +static_assert(is_subtype_of(Bottom[list[Any]], Top[list[int | Any]])) +static_assert(is_subtype_of(Bottom[list[int | Any]], Top[list[Any]])) +static_assert(is_subtype_of(Bottom[list[int | Any]], Top[list[int | str]])) +static_assert(is_subtype_of(Bottom[list[Intersection[int, Any]]], Top[list[Intersection[str, Any]]])) +static_assert(not is_subtype_of(Bottom[list[Intersection[int, bool | Any]]], Bottom[list[Intersection[str, Literal["x"] | Any]]])) + +# None and None +static_assert(not is_subtype_of(list[int], list[Any])) +static_assert(not is_subtype_of(list[Any], list[int])) +static_assert(is_subtype_of(list[int], list[int])) +static_assert(not is_subtype_of(list[int], list[object])) +static_assert(not is_subtype_of(list[object], list[int])) + +# Top and None +static_assert(not is_subtype_of(Top[list[Any]], list[Any])) +static_assert(not is_subtype_of(Top[list[Any]], list[int])) +static_assert(is_subtype_of(Top[list[int]], list[int])) + +# Bottom and None +static_assert(is_subtype_of(Bottom[list[Any]], list[object])) +static_assert(is_subtype_of(Bottom[list[int | Any]], list[str | int])) +static_assert(not is_subtype_of(Bottom[list[str | Any]], list[Intersection[int, bool | Any]])) + +# None and Bottom +static_assert(not is_subtype_of(list[int], Bottom[list[Any]])) +static_assert(not is_subtype_of(list[int], Bottom[list[int | Any]])) +static_assert(is_subtype_of(list[int], Bottom[list[int]])) + +# Top and Bottom +static_assert(not is_subtype_of(Top[list[Any]], Bottom[list[Any]])) +static_assert(not is_subtype_of(Top[list[int | Any]], Bottom[list[int | Any]])) +static_assert(is_subtype_of(Top[list[int]], Bottom[list[int]])) + +# Bottom and Bottom +static_assert(is_subtype_of(Bottom[list[Any]], Bottom[list[int | str | Any]])) +static_assert(is_subtype_of(Bottom[list[int | Any]], Bottom[list[int | str | Any]])) +static_assert(is_subtype_of(Bottom[list[bool | Any]], Bottom[list[int | Any]])) +static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[bool | Any]])) +static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[Any]])) +``` + +## Assignability + +### General + +Assignability is the same as subtyping for top and bottom materializations, because those are fully +static types, but some gradual types are assignable even if they are not subtypes. + +```py +from typing import Any, Literal +from ty_extensions import is_assignable_to, static_assert, Top, Intersection, Bottom + +# None and Top +static_assert(is_assignable_to(list[Any], Top[list[Any]])) +static_assert(is_assignable_to(list[int], Top[list[Any]])) +static_assert(not is_assignable_to(Top[list[Any]], list[int])) +static_assert(is_assignable_to(list[bool], Top[list[Intersection[int, Any]]])) +static_assert(is_assignable_to(list[int], Top[list[Intersection[int, Any]]])) +static_assert(is_assignable_to(list[Any], Top[list[Intersection[int, Any]]])) +static_assert(not is_assignable_to(list[int | str], Top[list[Intersection[int, Any]]])) +static_assert(not is_assignable_to(list[object], Top[list[Intersection[int, Any]]])) +static_assert(not is_assignable_to(list[str], Top[list[Intersection[int, Any]]])) +static_assert(not is_assignable_to(list[str | bool], Top[list[Intersection[int, Any]]])) + +# Top and Top +static_assert(is_assignable_to(Top[list[int | Any]], Top[list[Any]])) +static_assert(not is_assignable_to(Top[list[Any]], Top[list[int | Any]])) +static_assert(is_assignable_to(Top[list[Intersection[int, Any]]], Top[list[Any]])) +static_assert(not is_assignable_to(Top[list[Any]], Top[list[Intersection[int, Any]]])) +static_assert(not is_assignable_to(Top[list[Intersection[int, Any]]], Top[list[int | Any]])) +static_assert(not is_assignable_to(Top[list[int | Any]], Top[list[Intersection[int, Any]]])) +static_assert(not is_assignable_to(Top[list[str | Any]], Top[list[int | Any]])) +static_assert(is_assignable_to(Top[list[str | int | Any]], Top[list[int | Any]])) +static_assert(not is_assignable_to(Top[list[int | Any]], Top[list[str | int | Any]])) + +# Bottom and Top +static_assert(is_assignable_to(Bottom[list[Any]], Top[list[Any]])) +static_assert(is_assignable_to(Bottom[list[Any]], Top[list[int | Any]])) +static_assert(is_assignable_to(Bottom[list[int | Any]], Top[list[Any]])) +static_assert(is_assignable_to(Bottom[list[Intersection[int, Any]]], Top[list[Intersection[str, Any]]])) +static_assert( + not is_assignable_to(Bottom[list[Intersection[int, bool | Any]]], Bottom[list[Intersection[str, Literal["x"] | Any]]]) +) + +# None and None +static_assert(is_assignable_to(list[int], list[Any])) +static_assert(is_assignable_to(list[Any], list[int])) +static_assert(is_assignable_to(list[int], list[int])) +static_assert(not is_assignable_to(list[int], list[object])) +static_assert(not is_assignable_to(list[object], list[int])) + +# Top and None +static_assert(is_assignable_to(Top[list[Any]], list[Any])) +static_assert(not is_assignable_to(Top[list[Any]], list[int])) +static_assert(is_assignable_to(Top[list[int]], list[int])) + +# Bottom and None +static_assert(is_assignable_to(Bottom[list[Any]], list[object])) +static_assert(is_assignable_to(Bottom[list[int | Any]], Top[list[str | int]])) +static_assert(not is_assignable_to(Bottom[list[str | Any]], list[Intersection[int, bool | Any]])) + +# None and Bottom +static_assert(is_assignable_to(list[Any], Bottom[list[Any]])) +static_assert(not is_assignable_to(list[int], Bottom[list[Any]])) +static_assert(not is_assignable_to(list[int], Bottom[list[int | Any]])) +static_assert(is_assignable_to(list[int], Bottom[list[int]])) + +# Top and Bottom +static_assert(not is_assignable_to(Top[list[Any]], Bottom[list[Any]])) +static_assert(not is_assignable_to(Top[list[int | Any]], Bottom[list[int | Any]])) +static_assert(is_assignable_to(Top[list[int]], Bottom[list[int]])) + +# Bottom and Bottom +static_assert(is_assignable_to(Bottom[list[Any]], Bottom[list[int | str | Any]])) +static_assert(is_assignable_to(Bottom[list[int | Any]], Bottom[list[int | str | Any]])) +static_assert(is_assignable_to(Bottom[list[bool | Any]], Bottom[list[int | Any]])) +static_assert(not is_assignable_to(Bottom[list[int | Any]], Bottom[list[bool | Any]])) +static_assert(not is_assignable_to(Bottom[list[int | Any]], Bottom[list[Any]])) +``` + +### Subclasses with different variance + +We need to take special care when an invariant class inherits from a covariant or contravariant one. +This comes up frequently in practice because `list` (invariant) inherits from `Sequence` and a +number of other covariant ABCs, but we'll use a synthetic example. + +```py +from typing import Generic, TypeVar, Any +from ty_extensions import static_assert, is_assignable_to, is_equivalent_to, Top + +class A: + pass + +class B(A): + pass + +T_co = TypeVar("T_co", covariant=True) +T = TypeVar("T") + +class CovariantBase(Generic[T_co]): + def get(self) -> T_co: + raise NotImplementedError + +class InvariantChild(CovariantBase[T]): + def push(self, obj: T) -> None: ... + +static_assert(is_assignable_to(InvariantChild[A], CovariantBase[A])) +static_assert(is_assignable_to(InvariantChild[B], CovariantBase[A])) +static_assert(not is_assignable_to(InvariantChild[A], CovariantBase[B])) +static_assert(not is_assignable_to(InvariantChild[B], InvariantChild[A])) +static_assert(is_equivalent_to(Top[CovariantBase[Any]], CovariantBase[object])) +static_assert(is_assignable_to(InvariantChild[Any], CovariantBase[A])) + +static_assert(not is_assignable_to(Top[InvariantChild[Any]], CovariantBase[A])) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3217f0bf05..ec64c817dd 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -195,6 +195,34 @@ pub(crate) struct IsEquivalent; pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>; pub(crate) struct Normalized; +/// How a generic type has been specialized. +/// +/// This matters only if there is at least one invariant type parameter. +/// For example, we represent `Top[list[Any]]` as a `GenericAlias` with +/// `MaterializationKind` set to Top, which we denote as `Top[list[Any]]`. +/// A type `Top[list[T]]` includes all fully static list types `list[U]` where `U` is +/// a supertype of `Bottom[T]` and a subtype of `Top[T]`. +/// +/// Similarly, there is `Bottom[list[Any]]`. +/// This type is harder to make sense of in a set-theoretic framework, but +/// it is a subtype of all materializations of `list[Any]`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum MaterializationKind { + Top, + Bottom, +} + +impl MaterializationKind { + /// Flip the materialization type: `Top` becomes `Bottom` and vice versa. + #[must_use] + pub const fn flip(self) -> Self { + match self { + Self::Top => Self::Bottom, + Self::Bottom => Self::Top, + } + } +} + /// The descriptor protocol distinguishes two kinds of descriptors. Non-data descriptors /// define a `__get__` method, while data descriptors additionally define a `__set__` /// method or a `__delete__` method. This enum is used to categorize attributes into two @@ -489,11 +517,13 @@ impl<'db> PropertyInstanceType<'db> { } } - fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { Self::new( db, - self.getter(db).map(|ty| ty.materialize(db, variance)), - self.setter(db).map(|ty| ty.materialize(db, variance)), + self.getter(db) + .map(|ty| ty.materialize(db, materialization_kind)), + self.setter(db) + .map(|ty| ty.materialize(db, materialization_kind)), ) } } @@ -738,14 +768,14 @@ impl<'db> Type<'db> { /// 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) + self.materialize(db, MaterializationKind::Top) } /// 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) + self.materialize(db, MaterializationKind::Bottom) } /// If this type is an instance type where the class has a tuple spec, returns the tuple spec. @@ -780,29 +810,11 @@ impl<'db> Type<'db> { /// - 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> { + fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> 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(BoundTypeVarInstance::new( - db, - TypeVarInstance::new( - db, - Name::new_static("T_all"), - None, - None, - Some(variance), - None, - TypeVarKind::Pep695, - ), - BindingContext::Synthetic, - )), - TypeVarVariance::Covariant => Type::object(db), - TypeVarVariance::Contravariant => Type::Never, - TypeVarVariance::Bivariant => unreachable!(), + Type::Dynamic(_) => match materialization_kind { + MaterializationKind::Top => Type::object(db), + MaterializationKind::Bottom => Type::Never, }, Type::Never @@ -825,7 +837,7 @@ impl<'db> Type<'db> { | Type::BoundSuper(_) => *self, Type::PropertyInstance(property_instance) => { - Type::PropertyInstance(property_instance.materialize(db, variance)) + Type::PropertyInstance(property_instance.materialize(db, materialization_kind)) } Type::FunctionLiteral(_) | Type::BoundMethod(_) => { @@ -834,14 +846,16 @@ impl<'db> Type<'db> { *self } - Type::NominalInstance(instance) => instance.materialize(db, variance), + Type::NominalInstance(instance) => instance.materialize(db, materialization_kind), Type::GenericAlias(generic_alias) => { - Type::GenericAlias(generic_alias.materialize(db, variance)) + Type::GenericAlias(generic_alias.materialize(db, materialization_kind)) } Type::Callable(callable_type) => { - Type::Callable(callable_type.materialize(db, variance)) + Type::Callable(callable_type.materialize(db, materialization_kind)) + } + Type::SubclassOf(subclass_of_type) => { + subclass_of_type.materialize(db, materialization_kind) } - 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: @@ -850,35 +864,45 @@ impl<'db> Type<'db> { // > 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::ProtocolInstance(protocol_instance_type.materialize(db, materialization_kind)) + } + Type::Union(union_type) => { + union_type.map(db, |ty| ty.materialize(db, materialization_kind)) } - 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)), + .map(|ty| ty.materialize(db, materialization_kind)), ) .negative_elements( intersection_type .negative(db) .iter() - .map(|ty| ty.materialize(db, variance.flip())), + .map(|ty| ty.materialize(db, materialization_kind.flip())), ) .build(), - Type::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar.materialize(db, variance)), + Type::TypeVar(bound_typevar) => { + Type::TypeVar(bound_typevar.materialize(db, materialization_kind)) + } Type::NonInferableTypeVar(bound_typevar) => { - Type::NonInferableTypeVar(bound_typevar.materialize(db, variance)) + Type::NonInferableTypeVar(bound_typevar.materialize(db, materialization_kind)) } Type::TypeIs(type_is) => { - type_is.with_type(db, type_is.return_type(db).materialize(db, variance)) + // TODO(jelle): this seems wrong, should be invariant? + type_is.with_type( + db, + type_is + .return_type(db) + .materialize(db, materialization_kind), + ) } Type::TypedDict(_) => { // TODO: Materialization of gradual TypedDicts *self } - Type::TypeAlias(alias) => alias.value_type(db).materialize(db, variance), + Type::TypeAlias(alias) => alias.value_type(db).materialize(db, materialization_kind), } } @@ -6637,6 +6661,13 @@ impl<'db> TypeMapping<'_, 'db> { } } } + + fn materialization_kind(&self, db: &'db dyn Db) -> Option { + match self { + TypeMapping::Specialization(specialization) => specialization.materialization_kind(db), + _ => None, + } + } } /// Singleton types that are heavily special-cased by ty. Despite its name, @@ -7321,29 +7352,35 @@ impl<'db> TypeVarInstance<'db> { ) } - fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { Self::new( db, self.name(db), self.definition(db), self._bound_or_constraints(db) .and_then(|bound_or_constraints| match bound_or_constraints { - TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { - Some(bound_or_constraints.materialize(db, variance).into()) - } + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => Some( + bound_or_constraints + .materialize(db, materialization_kind) + .into(), + ), TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self .lazy_bound(db) - .map(|bound| bound.materialize(db, variance).into()), - TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self - .lazy_constraints(db) - .map(|constraints| constraints.materialize(db, variance).into()), + .map(|bound| bound.materialize(db, materialization_kind).into()), + TypeVarBoundOrConstraintsEvaluation::LazyConstraints => { + self.lazy_constraints(db).map(|constraints| { + constraints.materialize(db, materialization_kind).into() + }) + } }), self.explicit_variance(db), self._default(db).and_then(|default| match default { - TypeVarDefaultEvaluation::Eager(ty) => Some(ty.materialize(db, variance).into()), + TypeVarDefaultEvaluation::Eager(ty) => { + Some(ty.materialize(db, materialization_kind).into()) + } TypeVarDefaultEvaluation::Lazy => self .lazy_default(db) - .map(|ty| ty.materialize(db, variance).into()), + .map(|ty| ty.materialize(db, materialization_kind).into()), }), self.kind(db), ) @@ -7505,10 +7542,10 @@ impl<'db> BoundTypeVarInstance<'db> { ) } - fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { Self::new( db, - self.typevar(db).materialize(db, variance), + self.typevar(db).materialize(db, materialization_kind), self.binding_context(db), ) } @@ -7585,10 +7622,10 @@ impl<'db> TypeVarBoundOrConstraints<'db> { } } - fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { match self { TypeVarBoundOrConstraints::UpperBound(bound) => { - TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, variance)) + TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, materialization_kind)) } TypeVarBoundOrConstraints::Constraints(constraints) => { TypeVarBoundOrConstraints::Constraints(UnionType::new( @@ -7596,7 +7633,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> { constraints .elements(db) .iter() - .map(|ty| ty.materialize(db, variance)) + .map(|ty| ty.materialize(db, materialization_kind)) .collect::>() .into_boxed_slice(), )) @@ -8838,10 +8875,10 @@ impl<'db> CallableType<'db> { )) } - fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { CallableType::new( db, - self.signatures(db).materialize(db, variance), + self.signatures(db).materialize(db, materialization_kind), self.is_function_like(db), ) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a99a2af6ca..3bf1f4e679 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -31,10 +31,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def; use crate::types::{ ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor, - KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor, PropertyInstanceType, - StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, - TypeVarInstance, TypeVarKind, TypedDictParams, VarianceInferable, declaration_type, - infer_definition_types, todo_type, + KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, + PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, + TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, VarianceInferable, + declaration_type, infer_definition_types, todo_type, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -272,11 +272,16 @@ impl<'db> GenericAlias<'db> { ) } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { Self::new( db, self.origin(db), - self.specialization(db).materialize(db, variance), + self.specialization(db) + .materialize(db, materialization_kind), ) } @@ -404,10 +409,14 @@ impl<'db> ClassType<'db> { } } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { match self { Self::NonGeneric(_) => self, - Self::Generic(generic) => Self::Generic(generic.materialize(db, variance)), + Self::Generic(generic) => Self::Generic(generic.materialize(db, materialization_kind)), } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index ef5741ff5f..63c3447889 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -16,8 +16,8 @@ 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, UnionType, WrapperDescriptorKind, + CallableType, IntersectionType, KnownClass, MaterializationKind, MethodWrapperKind, Protocol, + StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind, }; use ruff_db::parsed::parsed_module; @@ -614,14 +614,25 @@ impl Display for DisplayGenericAlias<'_> { if let Some(tuple) = self.specialization.tuple(self.db) { tuple.display_with(self.db, self.settings).fmt(f) } else { + let prefix = match self.specialization.materialization_kind(self.db) { + None => "", + Some(MaterializationKind::Top) => "Top[", + Some(MaterializationKind::Bottom) => "Bottom[", + }; + let suffix = match self.specialization.materialization_kind(self.db) { + None => "", + Some(_) => "]", + }; write!( f, - "{origin}{specialization}", + "{prefix}{origin}{specialization}{suffix}", + prefix = prefix, origin = self.origin.name(self.db), specialization = self.specialization.display_short( self.db, TupleSpecialization::from_class(self.db, self.origin) ), + suffix = suffix, ) } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 2a686ef80a..2752340eea 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -16,9 +16,9 @@ use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, IsEquivalentVisitor, - KnownClass, KnownInstanceType, NormalizedVisitor, Type, TypeMapping, TypeRelation, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, binding_type, - declaration_type, + KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeMapping, + TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, + binding_type, declaration_type, }; use crate::{Db, FxOrderSet}; @@ -244,6 +244,7 @@ impl<'db> GenericContext<'db> { db, self, partial.types(db), + None, Some(TupleType::homogeneous(db, Type::unknown())), ) } else { @@ -304,7 +305,7 @@ impl<'db> GenericContext<'db> { types: Box<[Type<'db>]>, ) -> Specialization<'db> { assert!(self.variables(db).len() == types.len()); - Specialization::new(db, self, types, None) + Specialization::new(db, self, types, None, None) } /// Creates a specialization of this generic context for the `tuple` class. @@ -314,7 +315,7 @@ impl<'db> GenericContext<'db> { element_type: Type<'db>, tuple: TupleType<'db>, ) -> Specialization<'db> { - Specialization::new(db, self, Box::from([element_type]), Some(tuple)) + Specialization::new(db, self, Box::from([element_type]), None, Some(tuple)) } /// Creates a specialization of this generic context. Panics if the length of `types` does not @@ -360,7 +361,7 @@ impl<'db> GenericContext<'db> { expanded[idx] = default; } - Specialization::new(db, self, expanded.into_boxed_slice(), None) + Specialization::new(db, self, expanded.into_boxed_slice(), None, None) } pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { @@ -407,6 +408,14 @@ pub struct Specialization<'db> { pub(crate) generic_context: GenericContext<'db>, #[returns(deref)] pub(crate) types: Box<[Type<'db>]>, + /// The materialization kind of the specialization. For example, given an invariant + /// generic type `A`, `Top[A[Any]]` is a supertype of all materializations of `A[Any]`, + /// and is represented here with `Some(MaterializationKind::Top)`. Similarly, + /// `Bottom[A[Any]]` is a subtype of all materializations of `A[Any]`, and is represented + /// with `Some(MaterializationKind::Bottom)`. + /// The `materialization_kind` field may be non-`None` only if the specialization contains + /// dynamic types in invariant positions. + pub(crate) materialization_kind: Option, /// For specializations of `tuple`, we also store more detailed information about the tuple's /// elements, above what the class's (single) typevar can represent. @@ -430,6 +439,114 @@ pub(super) fn walk_specialization<'db, V: super::visitor::TypeVisitor<'db> + ?Si } } +fn is_subtype_in_invariant_position<'db, C: Constraints<'db>>( + db: &'db dyn Db, + derived_type: &Type<'db>, + derived_materialization: MaterializationKind, + base_type: &Type<'db>, + base_materialization: MaterializationKind, +) -> C { + let derived_top = derived_type.top_materialization(db); + let derived_bottom = derived_type.bottom_materialization(db); + let base_top = base_type.top_materialization(db); + let base_bottom = base_type.bottom_materialization(db); + match (derived_materialization, base_materialization) { + // `Derived` is a subtype of `Base` if the range of materializations covered by `Derived` + // is a subset of the range covered by `Base`. + (MaterializationKind::Top, MaterializationKind::Top) => C::from_bool( + db, + base_bottom.is_subtype_of(db, derived_bottom) + && derived_top.is_subtype_of(db, base_top), + ), + // One bottom is a subtype of another if it covers a strictly larger set of materializations. + (MaterializationKind::Bottom, MaterializationKind::Bottom) => C::from_bool( + db, + derived_bottom.is_subtype_of(db, base_bottom) + && base_top.is_subtype_of(db, derived_top), + ), + // The bottom materialization of `Derived` is a subtype of the top materialization + // of `Base` if there is some type that is both within the + // range of types covered by derived and within the range covered by base, because if such a type + // exists, it's a subtype of `Top[base]` and a supertype of `Bottom[derived]`. + (MaterializationKind::Bottom, MaterializationKind::Top) => C::from_bool( + db, + (base_bottom.is_subtype_of(db, derived_bottom) + && derived_bottom.is_subtype_of(db, base_top)) + || (base_bottom.is_subtype_of(db, derived_top) + && derived_top.is_subtype_of(db, base_top) + || (base_top.is_subtype_of(db, derived_top) + && derived_bottom.is_subtype_of(db, base_top))), + ), + // A top materialization is a subtype of a bottom materialization only if both original + // un-materialized types are the same fully static type. + (MaterializationKind::Top, MaterializationKind::Bottom) => C::from_bool( + db, + derived_top.is_subtype_of(db, base_bottom) + && base_top.is_subtype_of(db, derived_bottom), + ), + } +} + +/// Whether two types encountered in an invariant position +/// have a relation (subtyping or assignability), taking into account +/// that the two types may come from a top or bottom materialization. +fn has_relation_in_invariant_position<'db, C: Constraints<'db>>( + db: &'db dyn Db, + derived_type: &Type<'db>, + derived_materialization: Option, + base_type: &Type<'db>, + base_materialization: Option, + relation: TypeRelation, +) -> C { + match (derived_materialization, base_materialization, relation) { + // Top and bottom materializations are fully static types, so subtyping + // is the same as assignability. + (Some(derived_mat), Some(base_mat), _) => { + is_subtype_in_invariant_position(db, derived_type, derived_mat, base_type, base_mat) + } + // Subtyping between invariant type parameters without a top/bottom materialization involved + // is equivalence + (None, None, TypeRelation::Subtyping) => { + C::from_bool(db, derived_type.is_equivalent_to(db, *base_type)) + } + (None, None, TypeRelation::Assignability) => C::from_bool( + db, + derived_type.is_assignable_to(db, *base_type) + && base_type.is_assignable_to(db, *derived_type), + ), + // For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B] + (None, Some(base_mat), TypeRelation::Subtyping) => is_subtype_in_invariant_position( + db, + derived_type, + MaterializationKind::Top, + base_type, + base_mat, + ), + (Some(derived_mat), None, TypeRelation::Subtyping) => is_subtype_in_invariant_position( + db, + derived_type, + derived_mat, + base_type, + MaterializationKind::Bottom, + ), + // And A <~ B (assignability) is Bottom[A] <: Top[B] + (None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position( + db, + derived_type, + MaterializationKind::Bottom, + base_type, + base_mat, + ), + (Some(derived_mat), None, TypeRelation::Assignability) => is_subtype_in_invariant_position( + db, + derived_type, + derived_mat, + base_type, + MaterializationKind::Top, + ), + } +} + impl<'db> Specialization<'db> { /// Returns the tuple spec for a specialization of the `tuple` class. pub(crate) fn tuple(self, db: &'db dyn Db) -> Option<&'db TupleSpec<'db>> { @@ -481,15 +598,61 @@ impl<'db> Specialization<'db> { type_mapping: &TypeMapping<'a, 'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { + // TODO it seems like this should be possible to do in a much simpler way in + // `Self::apply_specialization`; just apply the type mapping to create the new + // specialization, then materialize the new specialization appropriately, if the type + // mapping is a materialization. But this doesn't work; see discussion in + // https://github.com/astral-sh/ruff/pull/20076 + let applied_materialization_kind = type_mapping.materialization_kind(db); + let mut has_dynamic_invariant_typevar = false; let types: Box<[_]> = self - .types(db) - .iter() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)) + .generic_context(db) + .variables(db) + .into_iter() + .zip(self.types(db)) + .map(|(bound_typevar, vartype)| { + let ty = vartype.apply_type_mapping_impl(db, type_mapping, visitor); + match (applied_materialization_kind, bound_typevar.variance(db)) { + (None, _) => ty, + (Some(_), TypeVarVariance::Bivariant) => + // With bivariance, all specializations are subtypes of each other, + // so any materialization is acceptable. + { + ty.materialize(db, MaterializationKind::Top) + } + (Some(materialization_kind), TypeVarVariance::Covariant) => { + ty.materialize(db, materialization_kind) + } + (Some(materialization_kind), TypeVarVariance::Contravariant) => { + ty.materialize(db, materialization_kind.flip()) + } + (Some(_), TypeVarVariance::Invariant) => { + let top_materialization = ty.materialize(db, MaterializationKind::Top); + if !ty.is_equivalent_to(db, top_materialization) { + has_dynamic_invariant_typevar = true; + } + ty + } + } + }) .collect(); + let tuple_inner = self .tuple_inner(db) .and_then(|tuple| tuple.apply_type_mapping_impl(db, type_mapping, visitor)); - Specialization::new(db, self.generic_context(db), types, tuple_inner) + let new_materialization_kind = if has_dynamic_invariant_typevar { + self.materialization_kind(db) + .or(applied_materialization_kind) + } else { + None + }; + Specialization::new( + db, + self.generic_context(db), + types, + new_materialization_kind, + tuple_inner, + ) } /// Applies an optional specialization to this specialization. @@ -527,7 +690,8 @@ impl<'db> Specialization<'db> { }) .collect(); // TODO: Combine the tuple specs too - Specialization::new(db, self.generic_context(db), types, None) + // TODO(jelle): specialization type? + Specialization::new(db, self.generic_context(db), types, None, None) } pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { @@ -540,25 +704,68 @@ impl<'db> Specialization<'db> { .tuple_inner(db) .and_then(|tuple| tuple.normalized_impl(db, visitor)); let context = self.generic_context(db).normalized_impl(db, visitor); - Self::new(db, context, types, tuple_inner) + Self::new( + db, + context, + types, + self.materialization_kind(db), + tuple_inner, + ) } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { + // The top and bottom materializations are fully static types already, so materializing them + // further does nothing. + if self.materialization_kind(db).is_some() { + return self; + } + let mut has_dynamic_invariant_typevar = false; let types: Box<[_]> = self .generic_context(db) .variables(db) .into_iter() .zip(self.types(db)) .map(|(bound_typevar, vartype)| { - let variance = bound_typevar.variance_with_polarity(db, variance); - vartype.materialize(db, variance) + match bound_typevar.variance(db) { + TypeVarVariance::Bivariant => { + // With bivariance, all specializations are subtypes of each other, + // so any materialization is acceptable. + vartype.materialize(db, MaterializationKind::Top) + } + TypeVarVariance::Covariant => vartype.materialize(db, materialization_kind), + TypeVarVariance::Contravariant => { + vartype.materialize(db, materialization_kind.flip()) + } + TypeVarVariance::Invariant => { + let top_materialization = vartype.materialize(db, MaterializationKind::Top); + if !vartype.is_equivalent_to(db, top_materialization) { + has_dynamic_invariant_typevar = true; + } + *vartype + } + } }) .collect(); let tuple_inner = self.tuple_inner(db).and_then(|tuple| { // Tuples are immutable, so tuple element types are always in covariant position. - tuple.materialize(db, variance) + tuple.materialize(db, materialization_kind) }); - Specialization::new(db, self.generic_context(db), types, tuple_inner) + let new_materialization_kind = if has_dynamic_invariant_typevar { + Some(materialization_kind) + } else { + None + }; + Specialization::new( + db, + self.generic_context(db), + types, + new_materialization_kind, + tuple_inner, + ) } pub(crate) fn has_relation_to_impl>( @@ -578,12 +785,20 @@ impl<'db> Specialization<'db> { return self_tuple.has_relation_to_impl(db, other_tuple, relation, visitor); } + let self_materialization_kind = self.materialization_kind(db); + let other_materialization_kind = other.materialization_kind(db); + let mut result = C::always_satisfiable(db); for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) .zip(self.types(db)) .zip(other.types(db)) { - if self_type.is_dynamic() || other_type.is_dynamic() { + // As an optimization, we can return early if either type is dynamic, unless + // we're dealing with a top or bottom materialization. + if other_materialization_kind.is_none() + && self_materialization_kind.is_none() + && (self_type.is_dynamic() || other_type.is_dynamic()) + { match relation { TypeRelation::Assignability => continue, TypeRelation::Subtyping => return C::unsatisfiable(db), @@ -597,14 +812,14 @@ impl<'db> Specialization<'db> { // - invariant: verify that self_type <: other_type AND other_type <: self_type // - bivariant: skip, can't make subtyping/assignability false let compatible = match bound_typevar.variance(db) { - TypeVarVariance::Invariant => match relation { - TypeRelation::Subtyping => self_type.when_equivalent_to(db, *other_type), - TypeRelation::Assignability => C::from_bool( - db, - self_type.is_assignable_to(db, *other_type) - && other_type.is_assignable_to(db, *self_type), - ), - }, + TypeVarVariance::Invariant => has_relation_in_invariant_position( + db, + self_type, + self_materialization_kind, + other_type, + other_materialization_kind, + relation, + ), TypeVarVariance::Covariant => { self_type.has_relation_to_impl(db, *other_type, relation, visitor) } @@ -627,6 +842,9 @@ impl<'db> Specialization<'db> { other: Specialization<'db>, visitor: &IsEquivalentVisitor<'db, C>, ) -> C { + if self.materialization_kind(db) != other.materialization_kind(db) { + return C::unsatisfiable(db); + } let generic_context = self.generic_context(db); if generic_context != other.generic_context(db) { return C::unsatisfiable(db); diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fb6bff18fc..825b767b55 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -13,7 +13,8 @@ use crate::types::protocol_class::walk_protocol_interface; use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::{ ApplyTypeMappingVisitor, ClassBase, HasRelationToVisitor, IsDisjointVisitor, - IsEquivalentVisitor, NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable, + IsEquivalentVisitor, MaterializationKind, NormalizedVisitor, TypeMapping, TypeRelation, + VarianceInferable, }; use crate::{Db, FxOrderSet}; @@ -259,11 +260,17 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Type<'db> { match self.0 { - NominalInstanceInner::ExactTuple(tuple) => Type::tuple(tuple.materialize(db, variance)), + NominalInstanceInner::ExactTuple(tuple) => { + Type::tuple(tuple.materialize(db, materialization_kind)) + } NominalInstanceInner::NonTuple(class) => { - Type::non_tuple_instance(class.materialize(db, variance)) + Type::non_tuple_instance(class.materialize(db, materialization_kind)) } } } @@ -577,12 +584,16 @@ impl<'db> ProtocolInstanceType<'db> { } } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> 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)) + Self::synthesized(synthesized.materialize(db, materialization_kind)) } } } @@ -668,8 +679,8 @@ mod synthesized_protocol { use crate::semantic_index::definition::Definition; use crate::types::protocol_class::ProtocolInterface; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, NormalizedVisitor, TypeMapping, - TypeVarVariance, VarianceInferable, + ApplyTypeMappingVisitor, BoundTypeVarInstance, MaterializationKind, NormalizedVisitor, + TypeMapping, TypeVarVariance, VarianceInferable, }; use crate::{Db, FxOrderSet}; @@ -696,8 +707,12 @@ mod synthesized_protocol { Self(interface.normalized_impl(db, visitor)) } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { - Self(self.0.materialize(db, variance)) + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { + Self(self.0.materialize(db, materialization_kind)) } pub(super) fn apply_type_mapping_impl<'a>( diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index ec1e394167..21c189e5c9 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -18,8 +18,9 @@ use crate::{ semantic_index::{definition::Definition, use_def_map}, types::{ BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, HasRelationToVisitor, - IsDisjointVisitor, KnownFunction, NormalizedVisitor, PropertyInstanceType, Signature, Type, - TypeMapping, TypeQualifiers, TypeRelation, VarianceInferable, + IsDisjointVisitor, KnownFunction, MaterializationKind, NormalizedVisitor, + PropertyInstanceType, Signature, Type, TypeMapping, TypeQualifiers, TypeRelation, + VarianceInferable, constraints::{Constraints, IteratorConstraintsExtension}, signatures::{Parameter, Parameters}, }, @@ -255,12 +256,16 @@ impl<'db> ProtocolInterface<'db> { ) } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { Self::new( db, self.inner(db) .iter() - .map(|(name, data)| (name.clone(), data.materialize(db, variance))) + .map(|(name, data)| (name.clone(), data.materialize(db, materialization_kind))) .collect::>(), ) } @@ -365,9 +370,9 @@ impl<'db> ProtocolMemberData<'db> { .find_legacy_typevars(db, binding_context, typevars); } - fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { Self { - kind: self.kind.materialize(db, variance), + kind: self.kind.materialize(db, materialization_kind), qualifiers: self.qualifiers, } } @@ -470,16 +475,16 @@ impl<'db> ProtocolMemberKind<'db> { } } - fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { match self { ProtocolMemberKind::Method(callable) => { - ProtocolMemberKind::Method(callable.materialize(db, variance)) + ProtocolMemberKind::Method(callable.materialize(db, materialization_kind)) } ProtocolMemberKind::Property(property) => { - ProtocolMemberKind::Property(property.materialize(db, variance)) + ProtocolMemberKind::Property(property.materialize(db, materialization_kind)) } ProtocolMemberKind::Other(ty) => { - ProtocolMemberKind::Other(ty.materialize(db, variance)) + ProtocolMemberKind::Other(ty.materialize(db, materialization_kind)) } } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 061c9b513d..abec350323 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -21,7 +21,8 @@ use crate::types::constraints::{Constraints, IteratorConstraintsExtension}; use crate::types::generics::{GenericContext, walk_generic_context}; use crate::types::{ BindingContext, BoundTypeVarInstance, HasRelationToVisitor, IsEquivalentVisitor, KnownClass, - NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable, todo_type, + MaterializationKind, NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable, + todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -57,11 +58,15 @@ impl<'db> CallableSignature<'db> { self.overloads.iter() } - pub(super) fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(super) fn materialize( + &self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { Self::from_overloads( self.overloads .iter() - .map(|signature| signature.materialize(db, variance)), + .map(|signature| signature.materialize(db, materialization_kind)), ) } @@ -405,17 +410,17 @@ impl<'db> Signature<'db> { self } - fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { Self { generic_context: self.generic_context, inherited_generic_context: self.inherited_generic_context, definition: self.definition, // Parameters are at contravariant position, so the variance is flipped. - parameters: self.parameters.materialize(db, variance.flip()), + parameters: self.parameters.materialize(db, materialization_kind.flip()), return_ty: Some( self.return_ty .unwrap_or(Type::unknown()) - .materialize(db, variance), + .materialize(db, materialization_kind), ), } } @@ -1063,13 +1068,13 @@ impl<'db> Parameters<'db> { } } - fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { if self.is_gradual { Parameters::object(db) } else { Parameters::new( self.iter() - .map(|parameter| parameter.materialize(db, variance)), + .map(|parameter| parameter.materialize(db, materialization_kind)), ) } } @@ -1395,12 +1400,12 @@ impl<'db> Parameter<'db> { self } - fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { Self { annotated_type: Some( self.annotated_type .unwrap_or(Type::unknown()) - .materialize(db, variance), + .materialize(db, materialization_kind), ), kind: self.kind.clone(), form: self.form, diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 63c7c13d51..6a49aa130c 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,17 +1,15 @@ -use ruff_python_ast::name::Name; - use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; use crate::types::constraints::Constraints; use crate::types::variance::VarianceInferable; use crate::types::{ - ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassType, DynamicType, - HasRelationToVisitor, IsDisjointVisitor, KnownClass, MemberLookupPolicy, NormalizedVisitor, - SpecialFormType, Type, TypeMapping, TypeRelation, TypeVarInstance, + ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType, HasRelationToVisitor, + IsDisjointVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, NormalizedVisitor, + SpecialFormType, Type, TypeMapping, TypeRelation, }; use crate::{Db, FxOrderSet}; -use super::{TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance}; +use super::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, get_size2::GetSize)] @@ -81,34 +79,15 @@ impl<'db> SubclassOfType<'db> { subclass_of.is_dynamic() } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> { + pub(super) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> 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::NonInferableTypeVar(BoundTypeVarInstance::new( - db, - TypeVarInstance::new( - db, - Name::new_static("T_all"), - None, - Some( - TypeVarBoundOrConstraints::UpperBound( - KnownClass::Type.to_instance(db), - ) - .into(), - ), - Some(variance), - None, - TypeVarKind::Pep695, - ), - BindingContext::Synthetic, - )) - } - TypeVarVariance::Bivariant => unreachable!(), + SubclassOfInner::Dynamic(_) => match materialization_kind { + MaterializationKind::Top => KnownClass::Type.to_instance(db), + MaterializationKind::Bottom => Type::Never, }, SubclassOfInner::Class(_) => Type::SubclassOf(self), } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 35de11013d..cc48fab9db 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -27,7 +27,7 @@ use crate::types::class::{ClassType, KnownClass}; use crate::types::constraints::{Constraints, IteratorConstraintsExtension}; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, IsDisjointVisitor, - IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, TypeVarVariance, + IsEquivalentVisitor, MaterializationKind, NormalizedVisitor, Type, TypeMapping, TypeRelation, UnionBuilder, UnionType, }; use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; @@ -228,8 +228,12 @@ impl<'db> TupleType<'db> { TupleType::new(db, &self.tuple(db).normalized_impl(db, visitor)) } - pub(crate) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Option { - TupleType::new(db, &self.tuple(db).materialize(db, variance)) + pub(crate) fn materialize( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Option { + TupleType::new(db, &self.tuple(db).materialize(db, materialization_kind)) } pub(crate) fn apply_type_mapping_impl<'a>( @@ -389,8 +393,12 @@ impl<'db> FixedLengthTuple> { Self::from_elements(self.0.iter().map(|ty| ty.normalized_impl(db, visitor))) } - fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { - Self::from_elements(self.0.iter().map(|ty| ty.materialize(db, variance))) + fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { + Self::from_elements( + self.0 + .iter() + .map(|ty| ty.materialize(db, materialization_kind)), + ) } fn apply_type_mapping_impl<'a>( @@ -703,11 +711,19 @@ impl<'db> VariableLengthTuple> { }) } - fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> TupleSpec<'db> { + fn materialize( + &self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> TupleSpec<'db> { Self::mixed( - self.prefix.iter().map(|ty| ty.materialize(db, variance)), - self.variable.materialize(db, variance), - self.suffix.iter().map(|ty| ty.materialize(db, variance)), + self.prefix + .iter() + .map(|ty| ty.materialize(db, materialization_kind)), + self.variable.materialize(db, materialization_kind), + self.suffix + .iter() + .map(|ty| ty.materialize(db, materialization_kind)), ) } @@ -1058,10 +1074,14 @@ impl<'db> Tuple> { } } - pub(crate) fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + pub(crate) fn materialize( + &self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { match self { - Tuple::Fixed(tuple) => Tuple::Fixed(tuple.materialize(db, variance)), - Tuple::Variable(tuple) => tuple.materialize(db, variance), + Tuple::Fixed(tuple) => Tuple::Fixed(tuple.materialize(db, materialization_kind)), + Tuple::Variable(tuple) => tuple.materialize(db, materialization_kind), } }