[ty] Introduce a representation for the top/bottom materialization of an invariant generic (#20076)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

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 <carl@astral.sh>
This commit is contained in:
Jelle Zijlstra 2025-08-27 17:53:57 -07:00 committed by GitHub
parent af259faed5
commit 18eaa659c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 696 additions and 191 deletions

View file

@ -47,7 +47,7 @@ The invariant position is replaced with an unresolved type variable.
```py ```py
def _(top_list: Top[list[Any]]): def _(top_list: Top[list[Any]]):
reveal_type(top_list) # revealed: list[T_all] reveal_type(top_list) # revealed: Top[list[Any]]
``` ```
### Bottom materialization ### Bottom materialization
@ -75,7 +75,7 @@ type variable.
```py ```py
def _(bottom_list: Bottom[list[Any]]): 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 ## Fully static types
@ -230,14 +230,14 @@ def _(
top_aiu: Top[LTAnyIntUnknown], top_aiu: Top[LTAnyIntUnknown],
bottom_aiu: Bottom[LTAnyIntUnknown], bottom_aiu: Bottom[LTAnyIntUnknown],
): ):
reveal_type(top_ai) # revealed: list[tuple[T_all, int]] reveal_type(top_ai) # revealed: Top[list[tuple[Any, int]]]
reveal_type(bottom_ai) # revealed: list[tuple[T_all, int]] reveal_type(bottom_ai) # revealed: Bottom[list[tuple[Any, int]]]
reveal_type(top_su) # revealed: list[tuple[str, T_all]] reveal_type(top_su) # revealed: Top[list[tuple[str, Unknown]]]
reveal_type(bottom_su) # revealed: list[tuple[str, T_all]] reveal_type(bottom_su) # revealed: Bottom[list[tuple[str, Unknown]]]
reveal_type(top_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: list[tuple[T_all, int, T_all]] reveal_type(bottom_aiu) # revealed: Bottom[list[tuple[Any, int, Unknown]]]
``` ```
## Union ## Union
@ -286,14 +286,14 @@ def _(
top_aiu: Top[list[Any | int | Unknown]], top_aiu: Top[list[Any | int | Unknown]],
bottom_aiu: Bottom[list[Any | int | Unknown]], bottom_aiu: Bottom[list[Any | int | Unknown]],
): ):
reveal_type(top_ai) # revealed: list[T_all | int] reveal_type(top_ai) # revealed: Top[list[Any | int]]
reveal_type(bottom_ai) # revealed: list[T_all | int] reveal_type(bottom_ai) # revealed: Bottom[list[Any | int]]
reveal_type(top_su) # revealed: list[str | T_all] reveal_type(top_su) # revealed: Top[list[str | Unknown]]
reveal_type(bottom_su) # revealed: list[str | T_all] reveal_type(bottom_su) # revealed: Bottom[list[str | Unknown]]
reveal_type(top_aiu) # revealed: list[T_all | int] reveal_type(top_aiu) # revealed: Top[list[Any | int]]
reveal_type(bottom_aiu) # revealed: list[T_all | int] reveal_type(bottom_aiu) # revealed: Bottom[list[Any | int]]
``` ```
## Intersection ## Intersection
@ -320,8 +320,10 @@ def _(
top: Top[Intersection[list[Any], list[int]]], top: Top[Intersection[list[Any], list[int]]],
bottom: Bottom[Intersection[list[Any], list[int]]], bottom: Bottom[Intersection[list[Any], list[int]]],
): ):
reveal_type(top) # revealed: list[T_all] & list[int] # Top[list[Any] & list[int]] = Top[list[Any]] & list[int] = list[int]
reveal_type(bottom) # revealed: list[T_all] & 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`) ## 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` # Here, `T` has an upper bound of `type`
def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]): def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]):
reveal_type(top) # revealed: list[T_all] reveal_type(top) # revealed: Top[list[type[Any]]]
reveal_type(bottom) # revealed: list[T_all] reveal_type(bottom) # revealed: Bottom[list[type[Any]]]
``` ```
## Type variables ## Type variables
@ -427,8 +429,8 @@ class GenericContravariant(Generic[T_contra]):
pass pass
def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]): def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]):
reveal_type(top) # revealed: GenericInvariant[T_all] reveal_type(top) # revealed: Top[GenericInvariant[Any]]
reveal_type(bottom) # revealed: GenericInvariant[T_all] reveal_type(bottom) # revealed: Bottom[GenericInvariant[Any]]
static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object])) static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object]))
static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never])) 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] type ContravariantCallable = Callable[[GenericContravariant[Any]], None]
def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None: def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None:
reveal_type(top) # revealed: (GenericInvariant[T_all], /) -> None reveal_type(top) # revealed: (Bottom[GenericInvariant[Any]], /) -> None
reveal_type(bottom) # revealed: (GenericInvariant[T_all], /) -> None reveal_type(bottom) # revealed: (Top[GenericInvariant[Any]], /) -> None
def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None: def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None:
reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None
@ -492,3 +494,207 @@ def _(
bottom_1: Bottom[1], # error: [invalid-type-form] 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]))
```

View file

@ -195,6 +195,34 @@ pub(crate) struct IsEquivalent;
pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>; pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>;
pub(crate) struct 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 /// The descriptor protocol distinguishes two kinds of descriptors. Non-data descriptors
/// define a `__get__` method, while data descriptors additionally define a `__set__` /// 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 /// 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( Self::new(
db, db,
self.getter(db).map(|ty| ty.materialize(db, variance)), self.getter(db)
self.setter(db).map(|ty| ty.materialize(db, variance)), .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. /// most general form of the type that is fully static.
#[must_use] #[must_use]
pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { 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 /// Returns the bottom materialization (or lower bound materialization) of this type, which is
/// the most specific form of the type that is fully static. /// the most specific form of the type that is fully static.
#[must_use] #[must_use]
pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { 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. /// 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 covariant position, it's replaced with `object`
/// - In contravariant position, it's replaced with `Never` /// - In contravariant position, it's replaced with `Never`
/// - In invariant position, it's replaced with an unresolved type variable /// - 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 { match self {
Type::Dynamic(_) => match variance { Type::Dynamic(_) => match materialization_kind {
// TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an MaterializationKind::Top => Type::object(db),
// existential type representing "all lists, containing any type." We currently MaterializationKind::Bottom => Type::Never,
// 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::Never Type::Never
@ -825,7 +837,7 @@ impl<'db> Type<'db> {
| Type::BoundSuper(_) => *self, | Type::BoundSuper(_) => *self,
Type::PropertyInstance(property_instance) => { Type::PropertyInstance(property_instance) => {
Type::PropertyInstance(property_instance.materialize(db, variance)) Type::PropertyInstance(property_instance.materialize(db, materialization_kind))
} }
Type::FunctionLiteral(_) | Type::BoundMethod(_) => { Type::FunctionLiteral(_) | Type::BoundMethod(_) => {
@ -834,14 +846,16 @@ impl<'db> Type<'db> {
*self *self
} }
Type::NominalInstance(instance) => instance.materialize(db, variance), Type::NominalInstance(instance) => instance.materialize(db, materialization_kind),
Type::GenericAlias(generic_alias) => { 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) => {
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) => { Type::ProtocolInstance(protocol_instance_type) => {
// TODO: Add tests for this once subtyping/assignability is implemented for // TODO: Add tests for this once subtyping/assignability is implemented for
// protocols. It _might_ require changing the logic here because: // 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; // > read-only property members, and method members, on protocols act covariantly;
// > write-only property members act contravariantly; and read/write attribute // > write-only property members act contravariantly; and read/write attribute
// > members on protocols act invariantly // > 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) Type::Intersection(intersection_type) => IntersectionBuilder::new(db)
.positive_elements( .positive_elements(
intersection_type intersection_type
.positive(db) .positive(db)
.iter() .iter()
.map(|ty| ty.materialize(db, variance)), .map(|ty| ty.materialize(db, materialization_kind)),
) )
.negative_elements( .negative_elements(
intersection_type intersection_type
.negative(db) .negative(db)
.iter() .iter()
.map(|ty| ty.materialize(db, variance.flip())), .map(|ty| ty.materialize(db, materialization_kind.flip())),
) )
.build(), .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) => {
Type::NonInferableTypeVar(bound_typevar.materialize(db, variance)) Type::NonInferableTypeVar(bound_typevar.materialize(db, materialization_kind))
} }
Type::TypeIs(type_is) => { 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(_) => { Type::TypedDict(_) => {
// TODO: Materialization of gradual TypedDicts // TODO: Materialization of gradual TypedDicts
*self *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<MaterializationKind> {
match self {
TypeMapping::Specialization(specialization) => specialization.materialization_kind(db),
_ => None,
}
}
} }
/// Singleton types that are heavily special-cased by ty. Despite its name, /// 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( Self::new(
db, db,
self.name(db), self.name(db),
self.definition(db), self.definition(db),
self._bound_or_constraints(db) self._bound_or_constraints(db)
.and_then(|bound_or_constraints| match bound_or_constraints { .and_then(|bound_or_constraints| match bound_or_constraints {
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => Some(
Some(bound_or_constraints.materialize(db, variance).into()) bound_or_constraints
} .materialize(db, materialization_kind)
.into(),
),
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self
.lazy_bound(db) .lazy_bound(db)
.map(|bound| bound.materialize(db, variance).into()), .map(|bound| bound.materialize(db, materialization_kind).into()),
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self TypeVarBoundOrConstraintsEvaluation::LazyConstraints => {
.lazy_constraints(db) self.lazy_constraints(db).map(|constraints| {
.map(|constraints| constraints.materialize(db, variance).into()), constraints.materialize(db, materialization_kind).into()
})
}
}), }),
self.explicit_variance(db), self.explicit_variance(db),
self._default(db).and_then(|default| match default { 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 TypeVarDefaultEvaluation::Lazy => self
.lazy_default(db) .lazy_default(db)
.map(|ty| ty.materialize(db, variance).into()), .map(|ty| ty.materialize(db, materialization_kind).into()),
}), }),
self.kind(db), 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( Self::new(
db, db,
self.typevar(db).materialize(db, variance), self.typevar(db).materialize(db, materialization_kind),
self.binding_context(db), 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 { match self {
TypeVarBoundOrConstraints::UpperBound(bound) => { TypeVarBoundOrConstraints::UpperBound(bound) => {
TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, variance)) TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, materialization_kind))
} }
TypeVarBoundOrConstraints::Constraints(constraints) => { TypeVarBoundOrConstraints::Constraints(constraints) => {
TypeVarBoundOrConstraints::Constraints(UnionType::new( TypeVarBoundOrConstraints::Constraints(UnionType::new(
@ -7596,7 +7633,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
constraints constraints
.elements(db) .elements(db)
.iter() .iter()
.map(|ty| ty.materialize(db, variance)) .map(|ty| ty.materialize(db, materialization_kind))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_boxed_slice(), .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( CallableType::new(
db, db,
self.signatures(db).materialize(db, variance), self.signatures(db).materialize(db, materialization_kind),
self.is_function_like(db), self.is_function_like(db),
) )
} }

View file

@ -31,10 +31,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType, ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor, DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor,
KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor, PropertyInstanceType, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor,
StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation,
TypeVarInstance, TypeVarKind, TypedDictParams, VarianceInferable, declaration_type, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, VarianceInferable,
infer_definition_types, todo_type, declaration_type, infer_definition_types, todo_type,
}; };
use crate::{ use crate::{
Db, FxIndexMap, FxOrderSet, Program, 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( Self::new(
db, db,
self.origin(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 { match self {
Self::NonGeneric(_) => self, Self::NonGeneric(_) => self,
Self::Generic(generic) => Self::Generic(generic.materialize(db, variance)), Self::Generic(generic) => Self::Generic(generic.materialize(db, materialization_kind)),
} }
} }

View file

@ -16,8 +16,8 @@ use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec; use crate::types::tuple::TupleSpec;
use crate::types::{ use crate::types::{
CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType, CallableType, IntersectionType, KnownClass, MaterializationKind, MethodWrapperKind, Protocol,
SubclassOfInner, Type, UnionType, WrapperDescriptorKind, StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind,
}; };
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
@ -614,14 +614,25 @@ impl Display for DisplayGenericAlias<'_> {
if let Some(tuple) = self.specialization.tuple(self.db) { if let Some(tuple) = self.specialization.tuple(self.db) {
tuple.display_with(self.db, self.settings).fmt(f) tuple.display_with(self.db, self.settings).fmt(f)
} else { } 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!( write!(
f, f,
"{origin}{specialization}", "{prefix}{origin}{specialization}{suffix}",
prefix = prefix,
origin = self.origin.name(self.db), origin = self.origin.name(self.db),
specialization = self.specialization.display_short( specialization = self.specialization.display_short(
self.db, self.db,
TupleSpecialization::from_class(self.db, self.origin) TupleSpecialization::from_class(self.db, self.origin)
), ),
suffix = suffix,
) )
} }
} }

View file

@ -16,9 +16,9 @@ use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, IsEquivalentVisitor, ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, IsEquivalentVisitor,
KnownClass, KnownInstanceType, NormalizedVisitor, Type, TypeMapping, TypeRelation, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeMapping,
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, binding_type, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType,
declaration_type, binding_type, declaration_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
@ -244,6 +244,7 @@ impl<'db> GenericContext<'db> {
db, db,
self, self,
partial.types(db), partial.types(db),
None,
Some(TupleType::homogeneous(db, Type::unknown())), Some(TupleType::homogeneous(db, Type::unknown())),
) )
} else { } else {
@ -304,7 +305,7 @@ impl<'db> GenericContext<'db> {
types: Box<[Type<'db>]>, types: Box<[Type<'db>]>,
) -> Specialization<'db> { ) -> Specialization<'db> {
assert!(self.variables(db).len() == types.len()); 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. /// Creates a specialization of this generic context for the `tuple` class.
@ -314,7 +315,7 @@ impl<'db> GenericContext<'db> {
element_type: Type<'db>, element_type: Type<'db>,
tuple: TupleType<'db>, tuple: TupleType<'db>,
) -> Specialization<'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 /// 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; 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 { 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>, pub(crate) generic_context: GenericContext<'db>,
#[returns(deref)] #[returns(deref)]
pub(crate) types: Box<[Type<'db>]>, 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<MaterializationKind>,
/// For specializations of `tuple`, we also store more detailed information about the tuple's /// For specializations of `tuple`, we also store more detailed information about the tuple's
/// elements, above what the class's (single) typevar can represent. /// 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<MaterializationKind>,
base_type: &Type<'db>,
base_materialization: Option<MaterializationKind>,
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> { impl<'db> Specialization<'db> {
/// Returns the tuple spec for a specialization of the `tuple` class. /// Returns the tuple spec for a specialization of the `tuple` class.
pub(crate) fn tuple(self, db: &'db dyn Db) -> Option<&'db TupleSpec<'db>> { 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>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>, visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self { ) -> 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 let types: Box<[_]> = self
.types(db) .generic_context(db)
.iter() .variables(db)
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)) .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(); .collect();
let tuple_inner = self let tuple_inner = self
.tuple_inner(db) .tuple_inner(db)
.and_then(|tuple| tuple.apply_type_mapping_impl(db, type_mapping, visitor)); .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. /// Applies an optional specialization to this specialization.
@ -527,7 +690,8 @@ impl<'db> Specialization<'db> {
}) })
.collect(); .collect();
// TODO: Combine the tuple specs too // 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 { 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) .tuple_inner(db)
.and_then(|tuple| tuple.normalized_impl(db, visitor)); .and_then(|tuple| tuple.normalized_impl(db, visitor));
let context = self.generic_context(db).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 let types: Box<[_]> = self
.generic_context(db) .generic_context(db)
.variables(db) .variables(db)
.into_iter() .into_iter()
.zip(self.types(db)) .zip(self.types(db))
.map(|(bound_typevar, vartype)| { .map(|(bound_typevar, vartype)| {
let variance = bound_typevar.variance_with_polarity(db, variance); match bound_typevar.variance(db) {
vartype.materialize(db, variance) 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(); .collect();
let tuple_inner = self.tuple_inner(db).and_then(|tuple| { let tuple_inner = self.tuple_inner(db).and_then(|tuple| {
// Tuples are immutable, so tuple element types are always in covariant position. // 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<C: Constraints<'db>>( pub(crate) fn has_relation_to_impl<C: Constraints<'db>>(
@ -578,12 +785,20 @@ impl<'db> Specialization<'db> {
return self_tuple.has_relation_to_impl(db, other_tuple, relation, visitor); 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); let mut result = C::always_satisfiable(db);
for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db)) .zip(self.types(db))
.zip(other.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 { match relation {
TypeRelation::Assignability => continue, TypeRelation::Assignability => continue,
TypeRelation::Subtyping => return C::unsatisfiable(db), 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 // - invariant: verify that self_type <: other_type AND other_type <: self_type
// - bivariant: skip, can't make subtyping/assignability false // - bivariant: skip, can't make subtyping/assignability false
let compatible = match bound_typevar.variance(db) { let compatible = match bound_typevar.variance(db) {
TypeVarVariance::Invariant => match relation { TypeVarVariance::Invariant => has_relation_in_invariant_position(
TypeRelation::Subtyping => self_type.when_equivalent_to(db, *other_type), db,
TypeRelation::Assignability => C::from_bool( self_type,
db, self_materialization_kind,
self_type.is_assignable_to(db, *other_type) other_type,
&& other_type.is_assignable_to(db, *self_type), other_materialization_kind,
), relation,
}, ),
TypeVarVariance::Covariant => { TypeVarVariance::Covariant => {
self_type.has_relation_to_impl(db, *other_type, relation, visitor) self_type.has_relation_to_impl(db, *other_type, relation, visitor)
} }
@ -627,6 +842,9 @@ impl<'db> Specialization<'db> {
other: Specialization<'db>, other: Specialization<'db>,
visitor: &IsEquivalentVisitor<'db, C>, visitor: &IsEquivalentVisitor<'db, C>,
) -> C { ) -> C {
if self.materialization_kind(db) != other.materialization_kind(db) {
return C::unsatisfiable(db);
}
let generic_context = self.generic_context(db); let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) { if generic_context != other.generic_context(db) {
return C::unsatisfiable(db); return C::unsatisfiable(db);

View file

@ -13,7 +13,8 @@ use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, ClassBase, HasRelationToVisitor, IsDisjointVisitor, ApplyTypeMappingVisitor, ClassBase, HasRelationToVisitor, IsDisjointVisitor,
IsEquivalentVisitor, NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable, IsEquivalentVisitor, MaterializationKind, NormalizedVisitor, TypeMapping, TypeRelation,
VarianceInferable,
}; };
use crate::{Db, FxOrderSet}; 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 { 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) => { 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 { match self.inner {
// TODO: This should also materialize via `class.materialize(db, variance)` // TODO: This should also materialize via `class.materialize(db, variance)`
Protocol::FromClass(class) => Self::from_class(class), Protocol::FromClass(class) => Self::from_class(class),
Protocol::Synthesized(synthesized) => { 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::semantic_index::definition::Definition;
use crate::types::protocol_class::ProtocolInterface; use crate::types::protocol_class::ProtocolInterface;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, NormalizedVisitor, TypeMapping, ApplyTypeMappingVisitor, BoundTypeVarInstance, MaterializationKind, NormalizedVisitor,
TypeVarVariance, VarianceInferable, TypeMapping, TypeVarVariance, VarianceInferable,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
@ -696,8 +707,12 @@ mod synthesized_protocol {
Self(interface.normalized_impl(db, visitor)) Self(interface.normalized_impl(db, visitor))
} }
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { pub(super) fn materialize(
Self(self.0.materialize(db, variance)) self,
db: &'db dyn Db,
materialization_kind: MaterializationKind,
) -> Self {
Self(self.0.materialize(db, materialization_kind))
} }
pub(super) fn apply_type_mapping_impl<'a>( pub(super) fn apply_type_mapping_impl<'a>(

View file

@ -18,8 +18,9 @@ use crate::{
semantic_index::{definition::Definition, use_def_map}, semantic_index::{definition::Definition, use_def_map},
types::{ types::{
BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, HasRelationToVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, HasRelationToVisitor,
IsDisjointVisitor, KnownFunction, NormalizedVisitor, PropertyInstanceType, Signature, Type, IsDisjointVisitor, KnownFunction, MaterializationKind, NormalizedVisitor,
TypeMapping, TypeQualifiers, TypeRelation, VarianceInferable, PropertyInstanceType, Signature, Type, TypeMapping, TypeQualifiers, TypeRelation,
VarianceInferable,
constraints::{Constraints, IteratorConstraintsExtension}, constraints::{Constraints, IteratorConstraintsExtension},
signatures::{Parameter, Parameters}, 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( Self::new(
db, db,
self.inner(db) self.inner(db)
.iter() .iter()
.map(|(name, data)| (name.clone(), data.materialize(db, variance))) .map(|(name, data)| (name.clone(), data.materialize(db, materialization_kind)))
.collect::<BTreeMap<_, _>>(), .collect::<BTreeMap<_, _>>(),
) )
} }
@ -365,9 +370,9 @@ impl<'db> ProtocolMemberData<'db> {
.find_legacy_typevars(db, binding_context, typevars); .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 { Self {
kind: self.kind.materialize(db, variance), kind: self.kind.materialize(db, materialization_kind),
qualifiers: self.qualifiers, 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 { match self {
ProtocolMemberKind::Method(callable) => { ProtocolMemberKind::Method(callable) => {
ProtocolMemberKind::Method(callable.materialize(db, variance)) ProtocolMemberKind::Method(callable.materialize(db, materialization_kind))
} }
ProtocolMemberKind::Property(property) => { ProtocolMemberKind::Property(property) => {
ProtocolMemberKind::Property(property.materialize(db, variance)) ProtocolMemberKind::Property(property.materialize(db, materialization_kind))
} }
ProtocolMemberKind::Other(ty) => { ProtocolMemberKind::Other(ty) => {
ProtocolMemberKind::Other(ty.materialize(db, variance)) ProtocolMemberKind::Other(ty.materialize(db, materialization_kind))
} }
} }
} }

View file

@ -21,7 +21,8 @@ use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::generics::{GenericContext, walk_generic_context}; use crate::types::generics::{GenericContext, walk_generic_context};
use crate::types::{ use crate::types::{
BindingContext, BoundTypeVarInstance, HasRelationToVisitor, IsEquivalentVisitor, KnownClass, BindingContext, BoundTypeVarInstance, HasRelationToVisitor, IsEquivalentVisitor, KnownClass,
NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable, todo_type, MaterializationKind, NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable,
todo_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name}; use ruff_python_ast::{self as ast, name::Name};
@ -57,11 +58,15 @@ impl<'db> CallableSignature<'db> {
self.overloads.iter() 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::from_overloads(
self.overloads self.overloads
.iter() .iter()
.map(|signature| signature.materialize(db, variance)), .map(|signature| signature.materialize(db, materialization_kind)),
) )
} }
@ -405,17 +410,17 @@ impl<'db> Signature<'db> {
self self
} }
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self {
Self { Self {
generic_context: self.generic_context, generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context, inherited_generic_context: self.inherited_generic_context,
definition: self.definition, definition: self.definition,
// Parameters are at contravariant position, so the variance is flipped. // 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( return_ty: Some(
self.return_ty self.return_ty
.unwrap_or(Type::unknown()) .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 { if self.is_gradual {
Parameters::object(db) Parameters::object(db)
} else { } else {
Parameters::new( Parameters::new(
self.iter() self.iter()
.map(|parameter| parameter.materialize(db, variance)), .map(|parameter| parameter.materialize(db, materialization_kind)),
) )
} }
} }
@ -1395,12 +1400,12 @@ impl<'db> Parameter<'db> {
self self
} }
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self {
Self { Self {
annotated_type: Some( annotated_type: Some(
self.annotated_type self.annotated_type
.unwrap_or(Type::unknown()) .unwrap_or(Type::unknown())
.materialize(db, variance), .materialize(db, materialization_kind),
), ),
kind: self.kind.clone(), kind: self.kind.clone(),
form: self.form, form: self.form,

View file

@ -1,17 +1,15 @@
use ruff_python_ast::name::Name;
use crate::place::PlaceAndQualifiers; use crate::place::PlaceAndQualifiers;
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::types::constraints::Constraints; use crate::types::constraints::Constraints;
use crate::types::variance::VarianceInferable; use crate::types::variance::VarianceInferable;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassType, DynamicType, ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType, HasRelationToVisitor,
HasRelationToVisitor, IsDisjointVisitor, KnownClass, MemberLookupPolicy, NormalizedVisitor, IsDisjointVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, NormalizedVisitor,
SpecialFormType, Type, TypeMapping, TypeRelation, TypeVarInstance, SpecialFormType, Type, TypeMapping, TypeRelation,
}; };
use crate::{Db, FxOrderSet}; 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`. /// 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
@ -81,34 +79,15 @@ impl<'db> SubclassOfType<'db> {
subclass_of.is_dynamic() 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 { match self.subclass_of {
SubclassOfInner::Dynamic(_) => match variance { SubclassOfInner::Dynamic(_) => match materialization_kind {
TypeVarVariance::Covariant => KnownClass::Type.to_instance(db), MaterializationKind::Top => KnownClass::Type.to_instance(db),
TypeVarVariance::Contravariant => Type::Never, MaterializationKind::Bottom => 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::Class(_) => Type::SubclassOf(self), SubclassOfInner::Class(_) => Type::SubclassOf(self),
} }

View file

@ -27,7 +27,7 @@ use crate::types::class::{ClassType, KnownClass};
use crate::types::constraints::{Constraints, IteratorConstraintsExtension}; use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, IsDisjointVisitor, ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, IsDisjointVisitor,
IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, TypeVarVariance, IsEquivalentVisitor, MaterializationKind, NormalizedVisitor, Type, TypeMapping, TypeRelation,
UnionBuilder, UnionType, UnionBuilder, UnionType,
}; };
use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; 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)) TupleType::new(db, &self.tuple(db).normalized_impl(db, visitor))
} }
pub(crate) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Option<Self> { pub(crate) fn materialize(
TupleType::new(db, &self.tuple(db).materialize(db, variance)) self,
db: &'db dyn Db,
materialization_kind: MaterializationKind,
) -> Option<Self> {
TupleType::new(db, &self.tuple(db).materialize(db, materialization_kind))
} }
pub(crate) fn apply_type_mapping_impl<'a>( pub(crate) fn apply_type_mapping_impl<'a>(
@ -389,8 +393,12 @@ impl<'db> FixedLengthTuple<Type<'db>> {
Self::from_elements(self.0.iter().map(|ty| ty.normalized_impl(db, visitor))) Self::from_elements(self.0.iter().map(|ty| ty.normalized_impl(db, visitor)))
} }
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { fn materialize(&self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self {
Self::from_elements(self.0.iter().map(|ty| ty.materialize(db, variance))) Self::from_elements(
self.0
.iter()
.map(|ty| ty.materialize(db, materialization_kind)),
)
} }
fn apply_type_mapping_impl<'a>( fn apply_type_mapping_impl<'a>(
@ -703,11 +711,19 @@ impl<'db> VariableLengthTuple<Type<'db>> {
}) })
} }
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::mixed(
self.prefix.iter().map(|ty| ty.materialize(db, variance)), self.prefix
self.variable.materialize(db, variance), .iter()
self.suffix.iter().map(|ty| ty.materialize(db, variance)), .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<Type<'db>> {
} }
} }
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 { match self {
Tuple::Fixed(tuple) => Tuple::Fixed(tuple.materialize(db, variance)), Tuple::Fixed(tuple) => Tuple::Fixed(tuple.materialize(db, materialization_kind)),
Tuple::Variable(tuple) => tuple.materialize(db, variance), Tuple::Variable(tuple) => tuple.materialize(db, materialization_kind),
} }
} }