[ty] Generate the top and bottom materialization of a type (#18594)

## Summary

This is to support https://github.com/astral-sh/ruff/pull/18607.

This PR adds support for generating the top materialization (or upper
bound materialization) and the bottom materialization (or lower bound
materialization) of a type. This is the most general and the most
specific form of the type which is fully static, respectively.
    
More concretely, `T'`, the top materialization of `T`, is the type `T`
with all occurrences
of dynamic type (`Any`, `Unknown`, `@Todo`) replaced as follows:

- In covariant position, it's replaced with `object`
- In contravariant position, it's replaced with `Never`
- In invariant position, it's replaced with an unresolved type variable

(For an invariant position, it should actually be replaced with an
existential type, but this is not currently representable in our type
system, so we use an unresolved type variable for now instead.)

The bottom materialization is implemented in the same way, except we
start out in "contravariant" position.

## Test Plan

Add test cases for various types.
This commit is contained in:
Dhruv Manilawala 2025-06-12 12:06:16 +05:30 committed by GitHub
parent f74527f4e9
commit ef4108af2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 798 additions and 3 deletions

View file

@ -0,0 +1,418 @@
# Materialization
There are two materializations of a type:
- The top materialization (or upper bound materialization) of a type, which is the most general form
of that type that is fully static
- The bottom materialization (or lower bound materialization) of a type, which is the most specific
form of that type that is fully static
More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of `Any` and
`Unknown` replaced as follows:
- In covariant position, it's replaced with `object`
- In contravariant position, it's replaced with `Never`
- In invariant position, it's replaced with an unresolved type variable
The top materialization starts from the covariant position while the bottom materialization starts
from the contravariant position.
TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an existential type
representing "all lists, containing any type". We currently represent this by replacing `Any` in
invariant position with an unresolved type variable.
## Replacement rules
### Top materialization
The dynamic type at the top-level is replaced with `object`.
```py
from typing import Any, Callable
from ty_extensions import Unknown, top_materialization
reveal_type(top_materialization(Any)) # revealed: object
reveal_type(top_materialization(Unknown)) # revealed: object
```
The contravariant position is replaced with `Never`.
```py
reveal_type(top_materialization(Callable[[Any], None])) # revealed: (Never, /) -> None
```
The invariant position is replaced with an unresolved type variable.
```py
reveal_type(top_materialization(list[Any])) # revealed: list[T_all]
```
### Bottom materialization
The dynamic type at the top-level is replaced with `Never`.
```py
from typing import Any, Callable
from ty_extensions import Unknown, bottom_materialization
reveal_type(bottom_materialization(Any)) # revealed: Never
reveal_type(bottom_materialization(Unknown)) # revealed: Never
```
The contravariant position is replaced with `object`.
```py
# revealed: (object, object, /) -> None
reveal_type(bottom_materialization(Callable[[Any, Unknown], None]))
```
The invariant position is replaced in the same way as the top materialization, with an unresolved
type variable.
```py
reveal_type(bottom_materialization(list[Any])) # revealed: list[T_all]
```
## Fully static types
The top / bottom (and only) materialization of any fully static type is just itself.
```py
from typing import Any, Literal
from ty_extensions import TypeOf, bottom_materialization, top_materialization
reveal_type(top_materialization(int)) # revealed: int
reveal_type(bottom_materialization(int)) # revealed: int
reveal_type(top_materialization(Literal[1])) # revealed: Literal[1]
reveal_type(bottom_materialization(Literal[1])) # revealed: Literal[1]
reveal_type(top_materialization(Literal[True])) # revealed: Literal[True]
reveal_type(bottom_materialization(Literal[True])) # revealed: Literal[True]
reveal_type(top_materialization(Literal["abc"])) # revealed: Literal["abc"]
reveal_type(bottom_materialization(Literal["abc"])) # revealed: Literal["abc"]
reveal_type(top_materialization(int | str)) # revealed: int | str
reveal_type(bottom_materialization(int | str)) # revealed: int | str
```
We currently treat function literals as fully static types, so they remain unchanged even though the
signature might have `Any` in it. (TODO: this is probably not right.)
```py
def function(x: Any) -> None: ...
class A:
def method(self, x: Any) -> None: ...
reveal_type(top_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None
reveal_type(bottom_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None
reveal_type(top_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None
reveal_type(bottom_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None
```
## Callable
For a callable, the parameter types are in a contravariant position, and the return type is in a
covariant position.
```py
from typing import Any, Callable
from ty_extensions import TypeOf, Unknown, bottom_materialization, top_materialization
def _(callable: Callable[[Any, Unknown], Any]) -> None:
# revealed: (Never, Never, /) -> object
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (object, object, /) -> Never
reveal_type(bottom_materialization(TypeOf[callable]))
```
The parameter types in a callable inherits the contravariant position.
```py
def _(callable: Callable[[int, tuple[int | Any]], tuple[Any]]) -> None:
# revealed: (int, tuple[int], /) -> tuple[object]
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (int, tuple[object], /) -> Never
reveal_type(bottom_materialization(TypeOf[callable]))
```
But, if the callable itself is in a contravariant position, then the variance is flipped i.e., if
the outer variance is covariant, it's flipped to contravariant, and if it's contravariant, it's
flipped to covariant, invariant remains invariant.
```py
def _(callable: Callable[[Any, Callable[[Unknown], Any]], Callable[[Any, int], Any]]) -> None:
# revealed: (Never, (object, /) -> Never, /) -> (Never, int, /) -> object
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (object, (Never, /) -> object, /) -> (object, int, /) -> Never
reveal_type(bottom_materialization(TypeOf[callable]))
```
## Tuple
All positions in a tuple are covariant.
```py
from typing import Any
from ty_extensions import Unknown, bottom_materialization, top_materialization
reveal_type(top_materialization(tuple[Any, int])) # revealed: tuple[object, int]
reveal_type(bottom_materialization(tuple[Any, int])) # revealed: Never
reveal_type(top_materialization(tuple[Unknown, int])) # revealed: tuple[object, int]
reveal_type(bottom_materialization(tuple[Unknown, int])) # revealed: Never
reveal_type(top_materialization(tuple[Any, int, Unknown])) # revealed: tuple[object, int, object]
reveal_type(bottom_materialization(tuple[Any, int, Unknown])) # revealed: Never
```
Except for when the tuple itself is in a contravariant position, then all positions in the tuple
inherit the contravariant position.
```py
from typing import Callable
from ty_extensions import TypeOf
def _(callable: Callable[[tuple[Any, int], tuple[str, Unknown]], None]) -> None:
# revealed: (Never, Never, /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (tuple[object, int], tuple[str, object], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
```
And, similarly for an invariant position.
```py
reveal_type(top_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]]
reveal_type(bottom_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]]
reveal_type(top_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]]
reveal_type(bottom_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]]
reveal_type(top_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]]
reveal_type(bottom_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]]
```
## Union
All positions in a union are covariant.
```py
from typing import Any
from ty_extensions import Unknown, bottom_materialization, top_materialization
reveal_type(top_materialization(Any | int)) # revealed: object
reveal_type(bottom_materialization(Any | int)) # revealed: int
reveal_type(top_materialization(Unknown | int)) # revealed: object
reveal_type(bottom_materialization(Unknown | int)) # revealed: int
reveal_type(top_materialization(int | str | Any)) # revealed: object
reveal_type(bottom_materialization(int | str | Any)) # revealed: int | str
```
Except for when the union itself is in a contravariant position, then all positions in the union
inherit the contravariant position.
```py
from typing import Callable
from ty_extensions import TypeOf
def _(callable: Callable[[Any | int, str | Unknown], None]) -> None:
# revealed: (int, str, /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (object, object, /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
```
And, similarly for an invariant position.
```py
reveal_type(top_materialization(list[Any | int])) # revealed: list[T_all | int]
reveal_type(bottom_materialization(list[Any | int])) # revealed: list[T_all | int]
reveal_type(top_materialization(list[str | Unknown])) # revealed: list[str | T_all]
reveal_type(bottom_materialization(list[str | Unknown])) # revealed: list[str | T_all]
reveal_type(top_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int]
reveal_type(bottom_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int]
```
## Intersection
All positions in an intersection are covariant.
```py
from typing import Any
from ty_extensions import Intersection, Unknown, bottom_materialization, top_materialization
reveal_type(top_materialization(Intersection[Any, int])) # revealed: int
reveal_type(bottom_materialization(Intersection[Any, int])) # revealed: Never
# Here, the top materialization of `Any | int` is `object` and the intersection of it with tuple
# revealed: tuple[str, object]
reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]]))
# revealed: Never
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
# revealed: int & tuple[str]
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str]]))
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
```
## Negation (via `Not`)
All positions in a negation are contravariant.
```py
from typing import Any
from ty_extensions import Not, Unknown, bottom_materialization, top_materialization
# ~Any is still Any, so the top materialization is object
reveal_type(top_materialization(Not[Any])) # revealed: object
reveal_type(bottom_materialization(Not[Any])) # revealed: Never
# tuple[Any, int] is in a contravariant position, so the
# top materialization is Never and the negation of it
# revealed: object
reveal_type(top_materialization(Not[tuple[Any, int]]))
# revealed: ~tuple[object, int]
reveal_type(bottom_materialization(Not[tuple[Any, int]]))
```
## `type`
```py
from typing import Any
from ty_extensions import Unknown, bottom_materialization, top_materialization
reveal_type(top_materialization(type[Any])) # revealed: type
reveal_type(bottom_materialization(type[Any])) # revealed: Never
reveal_type(top_materialization(type[Unknown])) # revealed: type
reveal_type(bottom_materialization(type[Unknown])) # revealed: Never
reveal_type(top_materialization(type[int | Any])) # revealed: type
reveal_type(bottom_materialization(type[int | Any])) # revealed: type[int]
# Here, `T` has an upper bound of `type`
reveal_type(top_materialization(list[type[Any]])) # revealed: list[T_all]
reveal_type(bottom_materialization(list[type[Any]])) # revealed: list[T_all]
```
## Type variables
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any, Never, TypeVar
from ty_extensions import (
TypeOf,
Unknown,
bottom_materialization,
top_materialization,
is_fully_static,
static_assert,
is_subtype_of,
)
def bounded_by_gradual[T: Any](t: T) -> None:
static_assert(not is_fully_static(T))
# Top materialization of `T: Any` is `T: object`
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
# Bottom materialization of `T: Any` is `T: Never`
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
# TODO: This should not error, see https://github.com/astral-sh/ty/issues/638
# error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never))
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
static_assert(not is_fully_static(T))
# Top materialization of `T: (int, Any)` is `T: (int, object)`
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
# Bottom materialization of `T: (int, Any)` is `T: (int, Never)`
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int))
```
## Generics
For generics, the materialization depends on the surrounding variance and the variance of the type
variable itself.
- If the type variable is invariant, the materialization happens in an invariant position
- If the type variable is covariant, the materialization happens as per the surrounding variance
- If the type variable is contravariant, the materialization happens as per the surrounding
variance, but the variance is flipped
```py
from typing import Any, Generic, TypeVar
from ty_extensions import bottom_materialization, top_materialization
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
T_contra = TypeVar("T_contra", contravariant=True)
class GenericInvariant(Generic[T]):
pass
class GenericCovariant(Generic[T_co]):
pass
class GenericContravariant(Generic[T_contra]):
pass
reveal_type(top_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all]
reveal_type(bottom_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all]
reveal_type(top_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[object]
reveal_type(bottom_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[Never]
reveal_type(top_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[Never]
reveal_type(bottom_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[object]
```
Parameters in callable are contravariant, so the variance should be flipped:
```py
from typing import Callable
from ty_extensions import TypeOf
def invariant(callable: Callable[[GenericInvariant[Any]], None]) -> None:
# revealed: (GenericInvariant[T_all], /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (GenericInvariant[T_all], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
def covariant(callable: Callable[[GenericCovariant[Any]], None]) -> None:
# revealed: (GenericCovariant[Never], /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (GenericCovariant[object], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
def contravariant(callable: Callable[[GenericContravariant[Any]], None]) -> None:
# revealed: (GenericContravariant[object], /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (GenericContravariant[Never], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
```

View file

@ -615,6 +615,120 @@ impl<'db> Type<'db> {
matches!(self, Type::Dynamic(_))
}
/// Returns the top materialization (or upper bound materialization) of this type, which is the
/// most general form of the type that is fully static.
#[must_use]
pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> {
self.materialize(db, TypeVarVariance::Covariant)
}
/// Returns the bottom materialization (or lower bound materialization) of this type, which is
/// the most specific form of the type that is fully static.
#[must_use]
pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> {
self.materialize(db, TypeVarVariance::Contravariant)
}
/// Returns the materialization of this type depending on the given `variance`.
///
/// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of
/// the dynamic types (`Any`, `Unknown`, `Todo`) replaced as follows:
///
/// - In covariant position, it's replaced with `object`
/// - In contravariant position, it's replaced with `Never`
/// - In invariant position, it's replaced with an unresolved type variable
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
match self {
Type::Dynamic(_) => match variance {
// TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an
// existential type representing "all lists, containing any type." We currently
// represent this by replacing `Any` in invariant position with an unresolved type
// variable.
TypeVarVariance::Invariant => Type::TypeVar(TypeVarInstance::new(
db,
Name::new_static("T_all"),
None,
None,
variance,
None,
TypeVarKind::Pep695,
)),
TypeVarVariance::Covariant => Type::object(db),
TypeVarVariance::Contravariant => Type::Never,
TypeVarVariance::Bivariant => unreachable!(),
},
Type::Never
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(_)
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_)
| Type::ClassLiteral(_)
| Type::BoundSuper(_) => *self,
Type::FunctionLiteral(_) | Type::BoundMethod(_) => {
// TODO: Subtyping between function / methods with a callable accounts for the
// signature (parameters and return type), so we might need to do something here
*self
}
Type::NominalInstance(nominal_instance_type) => {
Type::NominalInstance(nominal_instance_type.materialize(db, variance))
}
Type::GenericAlias(generic_alias) => {
Type::GenericAlias(generic_alias.materialize(db, variance))
}
Type::Callable(callable_type) => {
Type::Callable(callable_type.materialize(db, variance))
}
Type::SubclassOf(subclass_of_type) => subclass_of_type.materialize(db, variance),
Type::ProtocolInstance(protocol_instance_type) => {
// TODO: Add tests for this once subtyping/assignability is implemented for
// protocols. It _might_ require changing the logic here because:
//
// > Subtyping for protocol instances involves taking account of the fact that
// > read-only property members, and method members, on protocols act covariantly;
// > write-only property members act contravariantly; and read/write attribute
// > members on protocols act invariantly
Type::ProtocolInstance(protocol_instance_type.materialize(db, variance))
}
Type::Union(union_type) => union_type.map(db, |ty| ty.materialize(db, variance)),
Type::Intersection(intersection_type) => IntersectionBuilder::new(db)
.positive_elements(
intersection_type
.positive(db)
.iter()
.map(|ty| ty.materialize(db, variance)),
)
.negative_elements(
intersection_type
.negative(db)
.iter()
.map(|ty| ty.materialize(db, variance.flip())),
)
.build(),
Type::Tuple(tuple_type) => TupleType::from_elements(
db,
tuple_type
.elements(db)
.iter()
.map(|ty| ty.materialize(db, variance)),
),
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
}
}
/// Replace references to the class `class` with a self-reference marker. This is currently
/// used for recursive protocols, but could probably be extended to self-referential type-
/// aliases and similar.
@ -3634,6 +3748,21 @@ impl<'db> Type<'db> {
)
.into(),
Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => {
Binding::single(
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static(
"type",
)))
.type_form()
.with_annotated_type(Type::any())]),
Some(Type::any()),
),
)
.into()
}
Some(KnownFunction::AssertType) => Binding::single(
self,
Signature::new(
@ -5984,6 +6113,19 @@ impl<'db> TypeVarInstance<'db> {
self.kind(db),
)
}
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self::new(
db,
self.name(db),
self.definition(db),
self.bound_or_constraints(db)
.map(|b| b.materialize(db, variance)),
self.variance(db),
self.default_ty(db),
self.kind(db),
)
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
@ -5994,6 +6136,20 @@ pub enum TypeVarVariance {
Bivariant,
}
impl TypeVarVariance {
/// Flips the polarity of the variance.
///
/// Covariant becomes contravariant, contravariant becomes covariant, others remain unchanged.
pub(crate) const fn flip(self) -> Self {
match self {
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
TypeVarVariance::Covariant => TypeVarVariance::Contravariant,
TypeVarVariance::Contravariant => TypeVarVariance::Covariant,
TypeVarVariance::Bivariant => TypeVarVariance::Bivariant,
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub enum TypeVarBoundOrConstraints<'db> {
UpperBound(Type<'db>),
@ -6011,6 +6167,25 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
}
}
}
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
match self {
TypeVarBoundOrConstraints::UpperBound(bound) => {
TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, variance))
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
TypeVarBoundOrConstraints::Constraints(UnionType::new(
db,
constraints
.elements(db)
.iter()
.map(|ty| ty.materialize(db, variance))
.collect::<Vec<_>>()
.into_boxed_slice(),
))
}
}
}
}
/// Error returned if a type is not (or may not be) a context manager.
@ -7012,6 +7187,14 @@ impl<'db> CallableType<'db> {
))
}
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
CallableType::new(
db,
self.signatures(db).materialize(db, variance),
self.is_function_like(db),
)
}
/// Create a callable type which represents a fully-static "bottom" callable.
///
/// Specifically, this represents a callable type with a single signature:

View file

@ -675,6 +675,18 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::TopMaterialization) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(ty.top_materialization(db));
}
}
Some(KnownFunction::BottomMaterialization) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(ty.bottom_materialization(db));
}
}
Some(KnownFunction::Len) => {
if let [Some(first_arg)] = overload.parameter_types() {
if let Some(len_ty) = first_arg.len(db) {

View file

@ -1,6 +1,7 @@
use std::hash::BuildHasherDefault;
use std::sync::{LazyLock, Mutex};
use super::TypeVarVariance;
use super::{
IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType,
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, infer_expression_type,
@ -173,6 +174,14 @@ impl<'db> GenericAlias<'db> {
Self::new(db, self.origin(db), self.specialization(db).normalized(db))
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self::new(
db,
self.origin(db),
self.specialization(db).materialize(db, variance),
)
}
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
self.origin(db).definition(db)
}
@ -223,6 +232,13 @@ impl<'db> ClassType<'db> {
}
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
match self {
Self::NonGeneric(_) => self,
Self::Generic(generic) => Self::Generic(generic.materialize(db, variance)),
}
}
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
match self {
Self::NonGeneric(class) => class.has_pep_695_type_params(db),

View file

@ -890,6 +890,10 @@ pub enum KnownFunction {
DunderAllNames,
/// `ty_extensions.all_members`
AllMembers,
/// `ty_extensions.top_materialization`
TopMaterialization,
/// `ty_extensions.bottom_materialization`
BottomMaterialization,
}
impl KnownFunction {
@ -947,6 +951,8 @@ impl KnownFunction {
| Self::IsSingleValued
| Self::IsSingleton
| Self::IsSubtypeOf
| Self::TopMaterialization
| Self::BottomMaterialization
| Self::GenericContext
| Self::DunderAllNames
| Self::StaticAssert
@ -1007,6 +1013,8 @@ pub(crate) mod tests {
| KnownFunction::IsAssignableTo
| KnownFunction::IsEquivalentTo
| KnownFunction::IsGradualEquivalentTo
| KnownFunction::TopMaterialization
| KnownFunction::BottomMaterialization
| KnownFunction::AllMembers => KnownModule::TyExtensions,
};

View file

@ -358,6 +358,25 @@ impl<'db> Specialization<'db> {
Self::new(db, self.generic_context(db), types)
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
let types: Box<[_]> = self
.generic_context(db)
.variables(db)
.into_iter()
.zip(self.types(db))
.map(|(typevar, vartype)| {
let variance = match typevar.variance(db) {
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
TypeVarVariance::Covariant => variance,
TypeVarVariance::Contravariant => variance.flip(),
TypeVarVariance::Bivariant => unreachable!(),
};
vartype.materialize(db, variance)
})
.collect();
Specialization::new(db, self.generic_context(db), types)
}
pub(crate) fn has_relation_to(
self,
db: &'db dyn Db,

View file

@ -3,7 +3,7 @@
use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type};
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::{Boundness, Place, PlaceAndQualifiers};
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
use crate::{Db, FxOrderSet};
@ -80,6 +80,10 @@ impl<'db> NominalInstanceType<'db> {
Self::from_class(self.class.normalized(db))
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self::from_class(self.class.materialize(db, variance))
}
pub(super) fn has_relation_to(
self,
db: &'db dyn Db,
@ -314,6 +318,16 @@ impl<'db> ProtocolInstanceType<'db> {
}
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
match self.inner {
// TODO: This should also materialize via `class.materialize(db, variance)`
Protocol::FromClass(class) => Self::from_class(class),
Protocol::Synthesized(synthesized) => {
Self::synthesized(synthesized.materialize(db, variance))
}
}
}
pub(super) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
@ -370,7 +384,7 @@ impl<'db> Protocol<'db> {
mod synthesized_protocol {
use crate::types::protocol_class::ProtocolInterface;
use crate::types::{TypeMapping, TypeVarInstance};
use crate::types::{TypeMapping, TypeVarInstance, TypeVarVariance};
use crate::{Db, FxOrderSet};
/// A "synthesized" protocol type that is dissociated from a class definition in source code.
@ -390,6 +404,10 @@ mod synthesized_protocol {
Self(interface.normalized(db))
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self(self.0.materialize(db, variance))
}
pub(super) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,

View file

@ -303,4 +303,20 @@ mod flaky {
negation_reverses_subtype_order, db,
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
);
// Both the top and bottom materialization tests are flaky in part due to various failures that
// it discovers in the current implementation of assignability of the types.
// TODO: Create a issue with some example failures to keep track of it
// `T'`, the top materialization of `T`, should be assignable to `T`.
type_property_test!(
top_materialization_of_type_is_assignable_to_type, db,
forall types t. t.top_materialization(db).is_assignable_to(db, t)
);
// Similarly, `T'`, the bottom materialization of `T`, should also be assignable to `T`.
type_property_test!(
bottom_materialization_of_type_is_assigneble_to_type, db,
forall types t. t.bottom_materialization(db).is_assignable_to(db, t)
);
}

View file

@ -13,6 +13,8 @@ use crate::{
{Db, FxOrderSet},
};
use super::TypeVarVariance;
impl<'db> ClassLiteral<'db> {
/// Returns `Some` if this is a protocol class, `None` otherwise.
pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option<ProtocolClassLiteral<'db>> {
@ -177,6 +179,28 @@ impl<'db> ProtocolInterface<'db> {
}
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
match self {
Self::Members(members) => Self::Members(ProtocolInterfaceMembers::new(
db,
members
.inner(db)
.iter()
.map(|(name, data)| {
(
name.clone(),
ProtocolMemberData {
ty: data.ty.materialize(db, variance),
qualifiers: data.qualifiers,
},
)
})
.collect::<BTreeMap<_, _>>(),
)),
Self::SelfReference => Self::SelfReference,
}
}
pub(super) fn specialized_and_normalized<'a>(
self,
db: &'db dyn Db,

View file

@ -15,7 +15,7 @@ use std::{collections::HashMap, slice::Iter};
use itertools::EitherOrBoth;
use smallvec::{SmallVec, smallvec};
use super::{DynamicType, Type, definition_expression_type};
use super::{DynamicType, Type, TypeVarVariance, definition_expression_type};
use crate::semantic_index::definition::Definition;
use crate::types::generics::GenericContext;
use crate::types::{ClassLiteral, TypeMapping, TypeRelation, TypeVarInstance, todo_type};
@ -53,6 +53,14 @@ impl<'db> CallableSignature<'db> {
self.overloads.iter()
}
pub(super) fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self::from_overloads(
self.overloads
.iter()
.map(|signature| signature.materialize(db, variance)),
)
}
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
Self::from_overloads(
self.overloads
@ -353,6 +361,20 @@ impl<'db> Signature<'db> {
Self::new(Parameters::object(db), Some(Type::Never))
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
// Parameters are at contravariant position, so the variance is flipped.
parameters: self.parameters.materialize(db, variance.flip()),
return_ty: Some(
self.return_ty
.unwrap_or(Type::unknown())
.materialize(db, variance),
),
}
}
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
Self {
generic_context: self.generic_context.map(|ctx| ctx.normalized(db)),
@ -984,6 +1006,17 @@ impl<'db> Parameters<'db> {
}
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
if self.is_gradual {
Parameters::object(db)
} else {
Parameters::new(
self.iter()
.map(|parameter| parameter.materialize(db, variance)),
)
}
}
pub(crate) fn as_slice(&self) -> &[Parameter<'db>] {
self.value.as_slice()
}
@ -1304,6 +1337,18 @@ impl<'db> Parameter<'db> {
self
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self {
annotated_type: Some(
self.annotated_type
.unwrap_or(Type::unknown())
.materialize(db, variance),
),
kind: self.kind.clone(),
form: self.form,
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
Self {
annotated_type: self

View file

@ -1,3 +1,5 @@
use ruff_python_ast::name::Name;
use crate::place::PlaceAndQualifiers;
use crate::types::{
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation,
@ -5,6 +7,8 @@ use crate::types::{
};
use crate::{Db, FxOrderSet};
use super::{TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance};
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct SubclassOfType<'db> {
@ -73,6 +77,32 @@ impl<'db> SubclassOfType<'db> {
!self.is_dynamic()
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
match self.subclass_of {
SubclassOfInner::Dynamic(_) => match variance {
TypeVarVariance::Covariant => KnownClass::Type.to_instance(db),
TypeVarVariance::Contravariant => Type::Never,
TypeVarVariance::Invariant => {
// We need to materialize this to `type[T]` but that isn't representable so
// we instead use a type variable with an upper bound of `type`.
Type::TypeVar(TypeVarInstance::new(
db,
Name::new_static("T_all"),
None,
Some(TypeVarBoundOrConstraints::UpperBound(
KnownClass::Type.to_instance(db),
)),
variance,
None,
TypeVarKind::Pep695,
))
}
TypeVarVariance::Bivariant => unreachable!(),
},
SubclassOfInner::Class(_) => Type::SubclassOf(self),
}
}
pub(super) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,

View file

@ -44,6 +44,12 @@ def generic_context(type: Any) -> Any: ...
# either the module does not have `__all__` or it has invalid elements.
def dunder_all_names(module: Any) -> Any: ...
# Returns the type that's an upper bound of materializing the given (gradual) type.
def top_materialization(type: Any) -> Any: ...
# Returns the type that's a lower bound of materializing the given (gradual) type.
def bottom_materialization(type: Any) -> Any: ...
# Returns a tuple of all members of the given object, similar to `dir(obj)` and
# `inspect.getmembers(obj)`, with at least the following differences:
#