Skip to content

Commit ef4108a

Browse files
authored
[ty] Generate the top and bottom materialization of a type (#18594)
## Summary This is to support #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.
1 parent f74527f commit ef4108a

File tree

12 files changed

+798
-3
lines changed

12 files changed

+798
-3
lines changed

crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md

Lines changed: 418 additions & 0 deletions
Large diffs are not rendered by default.

crates/ty_python_semantic/src/types.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,120 @@ impl<'db> Type<'db> {
615615
matches!(self, Type::Dynamic(_))
616616
}
617617

618+
/// Returns the top materialization (or upper bound materialization) of this type, which is the
619+
/// most general form of the type that is fully static.
620+
#[must_use]
621+
pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> {
622+
self.materialize(db, TypeVarVariance::Covariant)
623+
}
624+
625+
/// Returns the bottom materialization (or lower bound materialization) of this type, which is
626+
/// the most specific form of the type that is fully static.
627+
#[must_use]
628+
pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> {
629+
self.materialize(db, TypeVarVariance::Contravariant)
630+
}
631+
632+
/// Returns the materialization of this type depending on the given `variance`.
633+
///
634+
/// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of
635+
/// the dynamic types (`Any`, `Unknown`, `Todo`) replaced as follows:
636+
///
637+
/// - In covariant position, it's replaced with `object`
638+
/// - In contravariant position, it's replaced with `Never`
639+
/// - In invariant position, it's replaced with an unresolved type variable
640+
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
641+
match self {
642+
Type::Dynamic(_) => match variance {
643+
// TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an
644+
// existential type representing "all lists, containing any type." We currently
645+
// represent this by replacing `Any` in invariant position with an unresolved type
646+
// variable.
647+
TypeVarVariance::Invariant => Type::TypeVar(TypeVarInstance::new(
648+
db,
649+
Name::new_static("T_all"),
650+
None,
651+
None,
652+
variance,
653+
None,
654+
TypeVarKind::Pep695,
655+
)),
656+
TypeVarVariance::Covariant => Type::object(db),
657+
TypeVarVariance::Contravariant => Type::Never,
658+
TypeVarVariance::Bivariant => unreachable!(),
659+
},
660+
661+
Type::Never
662+
| Type::WrapperDescriptor(_)
663+
| Type::MethodWrapper(_)
664+
| Type::DataclassDecorator(_)
665+
| Type::DataclassTransformer(_)
666+
| Type::ModuleLiteral(_)
667+
| Type::IntLiteral(_)
668+
| Type::BooleanLiteral(_)
669+
| Type::StringLiteral(_)
670+
| Type::LiteralString
671+
| Type::BytesLiteral(_)
672+
| Type::SpecialForm(_)
673+
| Type::KnownInstance(_)
674+
| Type::AlwaysFalsy
675+
| Type::AlwaysTruthy
676+
| Type::PropertyInstance(_)
677+
| Type::ClassLiteral(_)
678+
| Type::BoundSuper(_) => *self,
679+
680+
Type::FunctionLiteral(_) | Type::BoundMethod(_) => {
681+
// TODO: Subtyping between function / methods with a callable accounts for the
682+
// signature (parameters and return type), so we might need to do something here
683+
*self
684+
}
685+
686+
Type::NominalInstance(nominal_instance_type) => {
687+
Type::NominalInstance(nominal_instance_type.materialize(db, variance))
688+
}
689+
Type::GenericAlias(generic_alias) => {
690+
Type::GenericAlias(generic_alias.materialize(db, variance))
691+
}
692+
Type::Callable(callable_type) => {
693+
Type::Callable(callable_type.materialize(db, variance))
694+
}
695+
Type::SubclassOf(subclass_of_type) => subclass_of_type.materialize(db, variance),
696+
Type::ProtocolInstance(protocol_instance_type) => {
697+
// TODO: Add tests for this once subtyping/assignability is implemented for
698+
// protocols. It _might_ require changing the logic here because:
699+
//
700+
// > Subtyping for protocol instances involves taking account of the fact that
701+
// > read-only property members, and method members, on protocols act covariantly;
702+
// > write-only property members act contravariantly; and read/write attribute
703+
// > members on protocols act invariantly
704+
Type::ProtocolInstance(protocol_instance_type.materialize(db, variance))
705+
}
706+
Type::Union(union_type) => union_type.map(db, |ty| ty.materialize(db, variance)),
707+
Type::Intersection(intersection_type) => IntersectionBuilder::new(db)
708+
.positive_elements(
709+
intersection_type
710+
.positive(db)
711+
.iter()
712+
.map(|ty| ty.materialize(db, variance)),
713+
)
714+
.negative_elements(
715+
intersection_type
716+
.negative(db)
717+
.iter()
718+
.map(|ty| ty.materialize(db, variance.flip())),
719+
)
720+
.build(),
721+
Type::Tuple(tuple_type) => TupleType::from_elements(
722+
db,
723+
tuple_type
724+
.elements(db)
725+
.iter()
726+
.map(|ty| ty.materialize(db, variance)),
727+
),
728+
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
729+
}
730+
}
731+
618732
/// Replace references to the class `class` with a self-reference marker. This is currently
619733
/// used for recursive protocols, but could probably be extended to self-referential type-
620734
/// aliases and similar.
@@ -3634,6 +3748,21 @@ impl<'db> Type<'db> {
36343748
)
36353749
.into(),
36363750

3751+
Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => {
3752+
Binding::single(
3753+
self,
3754+
Signature::new(
3755+
Parameters::new([Parameter::positional_only(Some(Name::new_static(
3756+
"type",
3757+
)))
3758+
.type_form()
3759+
.with_annotated_type(Type::any())]),
3760+
Some(Type::any()),
3761+
),
3762+
)
3763+
.into()
3764+
}
3765+
36373766
Some(KnownFunction::AssertType) => Binding::single(
36383767
self,
36393768
Signature::new(
@@ -5984,6 +6113,19 @@ impl<'db> TypeVarInstance<'db> {
59846113
self.kind(db),
59856114
)
59866115
}
6116+
6117+
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
6118+
Self::new(
6119+
db,
6120+
self.name(db),
6121+
self.definition(db),
6122+
self.bound_or_constraints(db)
6123+
.map(|b| b.materialize(db, variance)),
6124+
self.variance(db),
6125+
self.default_ty(db),
6126+
self.kind(db),
6127+
)
6128+
}
59876129
}
59886130

59896131
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
@@ -5994,6 +6136,20 @@ pub enum TypeVarVariance {
59946136
Bivariant,
59956137
}
59966138

6139+
impl TypeVarVariance {
6140+
/// Flips the polarity of the variance.
6141+
///
6142+
/// Covariant becomes contravariant, contravariant becomes covariant, others remain unchanged.
6143+
pub(crate) const fn flip(self) -> Self {
6144+
match self {
6145+
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
6146+
TypeVarVariance::Covariant => TypeVarVariance::Contravariant,
6147+
TypeVarVariance::Contravariant => TypeVarVariance::Covariant,
6148+
TypeVarVariance::Bivariant => TypeVarVariance::Bivariant,
6149+
}
6150+
}
6151+
}
6152+
59976153
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
59986154
pub enum TypeVarBoundOrConstraints<'db> {
59996155
UpperBound(Type<'db>),
@@ -6011,6 +6167,25 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
60116167
}
60126168
}
60136169
}
6170+
6171+
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
6172+
match self {
6173+
TypeVarBoundOrConstraints::UpperBound(bound) => {
6174+
TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, variance))
6175+
}
6176+
TypeVarBoundOrConstraints::Constraints(constraints) => {
6177+
TypeVarBoundOrConstraints::Constraints(UnionType::new(
6178+
db,
6179+
constraints
6180+
.elements(db)
6181+
.iter()
6182+
.map(|ty| ty.materialize(db, variance))
6183+
.collect::<Vec<_>>()
6184+
.into_boxed_slice(),
6185+
))
6186+
}
6187+
}
6188+
}
60146189
}
60156190

60166191
/// Error returned if a type is not (or may not be) a context manager.
@@ -7012,6 +7187,14 @@ impl<'db> CallableType<'db> {
70127187
))
70137188
}
70147189

7190+
fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
7191+
CallableType::new(
7192+
db,
7193+
self.signatures(db).materialize(db, variance),
7194+
self.is_function_like(db),
7195+
)
7196+
}
7197+
70157198
/// Create a callable type which represents a fully-static "bottom" callable.
70167199
///
70177200
/// Specifically, this represents a callable type with a single signature:

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,18 @@ impl<'db> Bindings<'db> {
675675
}
676676
}
677677

678+
Some(KnownFunction::TopMaterialization) => {
679+
if let [Some(ty)] = overload.parameter_types() {
680+
overload.set_return_type(ty.top_materialization(db));
681+
}
682+
}
683+
684+
Some(KnownFunction::BottomMaterialization) => {
685+
if let [Some(ty)] = overload.parameter_types() {
686+
overload.set_return_type(ty.bottom_materialization(db));
687+
}
688+
}
689+
678690
Some(KnownFunction::Len) => {
679691
if let [Some(first_arg)] = overload.parameter_types() {
680692
if let Some(len_ty) = first_arg.len(db) {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::hash::BuildHasherDefault;
22
use std::sync::{LazyLock, Mutex};
33

4+
use super::TypeVarVariance;
45
use super::{
56
IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType,
67
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, infer_expression_type,
@@ -173,6 +174,14 @@ impl<'db> GenericAlias<'db> {
173174
Self::new(db, self.origin(db), self.specialization(db).normalized(db))
174175
}
175176

177+
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
178+
Self::new(
179+
db,
180+
self.origin(db),
181+
self.specialization(db).materialize(db, variance),
182+
)
183+
}
184+
176185
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
177186
self.origin(db).definition(db)
178187
}
@@ -223,6 +232,13 @@ impl<'db> ClassType<'db> {
223232
}
224233
}
225234

235+
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
236+
match self {
237+
Self::NonGeneric(_) => self,
238+
Self::Generic(generic) => Self::Generic(generic.materialize(db, variance)),
239+
}
240+
}
241+
226242
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
227243
match self {
228244
Self::NonGeneric(class) => class.has_pep_695_type_params(db),

crates/ty_python_semantic/src/types/function.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,10 @@ pub enum KnownFunction {
890890
DunderAllNames,
891891
/// `ty_extensions.all_members`
892892
AllMembers,
893+
/// `ty_extensions.top_materialization`
894+
TopMaterialization,
895+
/// `ty_extensions.bottom_materialization`
896+
BottomMaterialization,
893897
}
894898

895899
impl KnownFunction {
@@ -947,6 +951,8 @@ impl KnownFunction {
947951
| Self::IsSingleValued
948952
| Self::IsSingleton
949953
| Self::IsSubtypeOf
954+
| Self::TopMaterialization
955+
| Self::BottomMaterialization
950956
| Self::GenericContext
951957
| Self::DunderAllNames
952958
| Self::StaticAssert
@@ -1007,6 +1013,8 @@ pub(crate) mod tests {
10071013
| KnownFunction::IsAssignableTo
10081014
| KnownFunction::IsEquivalentTo
10091015
| KnownFunction::IsGradualEquivalentTo
1016+
| KnownFunction::TopMaterialization
1017+
| KnownFunction::BottomMaterialization
10101018
| KnownFunction::AllMembers => KnownModule::TyExtensions,
10111019
};
10121020

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,25 @@ impl<'db> Specialization<'db> {
358358
Self::new(db, self.generic_context(db), types)
359359
}
360360

361+
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
362+
let types: Box<[_]> = self
363+
.generic_context(db)
364+
.variables(db)
365+
.into_iter()
366+
.zip(self.types(db))
367+
.map(|(typevar, vartype)| {
368+
let variance = match typevar.variance(db) {
369+
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
370+
TypeVarVariance::Covariant => variance,
371+
TypeVarVariance::Contravariant => variance.flip(),
372+
TypeVarVariance::Bivariant => unreachable!(),
373+
};
374+
vartype.materialize(db, variance)
375+
})
376+
.collect();
377+
Specialization::new(db, self.generic_context(db), types)
378+
}
379+
361380
pub(crate) fn has_relation_to(
362381
self,
363382
db: &'db dyn Db,

crates/ty_python_semantic/src/types/instance.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::marker::PhantomData;
44

55
use super::protocol_class::ProtocolInterface;
6-
use super::{ClassType, KnownClass, SubclassOfType, Type};
6+
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
77
use crate::place::{Boundness, Place, PlaceAndQualifiers};
88
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
99
use crate::{Db, FxOrderSet};
@@ -80,6 +80,10 @@ impl<'db> NominalInstanceType<'db> {
8080
Self::from_class(self.class.normalized(db))
8181
}
8282

83+
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
84+
Self::from_class(self.class.materialize(db, variance))
85+
}
86+
8387
pub(super) fn has_relation_to(
8488
self,
8589
db: &'db dyn Db,
@@ -314,6 +318,16 @@ impl<'db> ProtocolInstanceType<'db> {
314318
}
315319
}
316320

321+
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
322+
match self.inner {
323+
// TODO: This should also materialize via `class.materialize(db, variance)`
324+
Protocol::FromClass(class) => Self::from_class(class),
325+
Protocol::Synthesized(synthesized) => {
326+
Self::synthesized(synthesized.materialize(db, variance))
327+
}
328+
}
329+
}
330+
317331
pub(super) fn apply_type_mapping<'a>(
318332
self,
319333
db: &'db dyn Db,
@@ -370,7 +384,7 @@ impl<'db> Protocol<'db> {
370384

371385
mod synthesized_protocol {
372386
use crate::types::protocol_class::ProtocolInterface;
373-
use crate::types::{TypeMapping, TypeVarInstance};
387+
use crate::types::{TypeMapping, TypeVarInstance, TypeVarVariance};
374388
use crate::{Db, FxOrderSet};
375389

376390
/// A "synthesized" protocol type that is dissociated from a class definition in source code.
@@ -390,6 +404,10 @@ mod synthesized_protocol {
390404
Self(interface.normalized(db))
391405
}
392406

407+
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
408+
Self(self.0.materialize(db, variance))
409+
}
410+
393411
pub(super) fn apply_type_mapping<'a>(
394412
self,
395413
db: &'db dyn Db,

0 commit comments

Comments
 (0)