From 18eaa659c1ea9a03bee798e161d2f2db454e154f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 27 Aug 2025 17:53:57 -0700 Subject: [PATCH] [ty] Introduce a representation for the top/bottom materialization of an invariant generic (#20076) Part of #994. This adds a new field to the Specialization struct to record when we're dealing with the top or bottom materialization of an invariant generic. It also implements subtyping and assignability for these objects. Next planned steps after this is done are to implement other operations on top/bottom materializations; probably attribute access is an important one. --------- Co-authored-by: Carl Meyer --- .../mdtest/type_properties/materialization.md | 250 ++++++++++++++-- crates/ty_python_semantic/src/types.rs | 151 ++++++---- crates/ty_python_semantic/src/types/class.rs | 25 +- .../ty_python_semantic/src/types/display.rs | 17 +- .../ty_python_semantic/src/types/generics.rs | 270 ++++++++++++++++-- .../ty_python_semantic/src/types/instance.rs | 35 ++- .../src/types/protocol_class.rs | 25 +- .../src/types/signatures.rs | 25 +- .../src/types/subclass_of.rs | 45 +-- crates/ty_python_semantic/src/types/tuple.rs | 44 ++- 10 files changed, 696 insertions(+), 191 deletions(-) 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), } }