[ty] Handle typevars that have other typevars as a default (#17956)

It's possible for a typevar to list another typevar as its default
value:

```py
class C[T, U = T]: ...
```

When specializing this class, if a type isn't provided for `U`, we would
previously use the default as-is, leaving an unspecialized `T` typevar
in the specialization. Instead, we want to use what `T` is mapped to as
the type of `U`.

```py
reveal_type(C())  # revealed: C[Unknown, Unknown]
reveal_type(C[int]())  # revealed: C[int, int]
reveal_type(C[int, str]())  # revealed: C[int, str]
```

This is especially important for the `slice` built-in type.
This commit is contained in:
Douglas Creager 2025-05-08 19:01:27 -04:00 committed by GitHub
parent f51f1f7153
commit b705664d49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 226 additions and 102 deletions

View file

@ -45,7 +45,7 @@ use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::generics::{GenericContext, Specialization, TypeMapping};
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
@ -342,13 +342,13 @@ pub struct PropertyInstanceType<'db> {
}
impl<'db> PropertyInstanceType<'db> {
fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
let getter = self
.getter(db)
.map(|ty| ty.apply_specialization(db, specialization));
.map(|ty| ty.apply_type_mapping(db, type_mapping));
let setter = self
.setter(db)
.map(|ty| ty.apply_specialization(db, specialization));
.map(|ty| ty.apply_type_mapping(db, type_mapping));
Self::new(db, getter, setter)
}
@ -5043,75 +5043,83 @@ impl<'db> Type<'db> {
self,
db: &'db dyn Db,
specialization: Specialization<'db>,
) -> Type<'db> {
self.apply_type_mapping(db, specialization.type_mapping())
}
fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
type_mapping: TypeMapping<'a, 'db>,
) -> Type<'db> {
match self {
Type::TypeVar(typevar) => specialization.get(db, typevar).unwrap_or(self),
Type::TypeVar(typevar) => type_mapping.get(db, typevar).unwrap_or(self),
Type::FunctionLiteral(function) => {
Type::FunctionLiteral(function.apply_specialization(db, specialization))
Type::FunctionLiteral(function.apply_type_mapping(db, type_mapping))
}
Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new(
db,
method.function(db).apply_specialization(db, specialization),
method.self_instance(db).apply_specialization(db, specialization),
method.function(db).apply_type_mapping(db, type_mapping),
method.self_instance(db).apply_type_mapping(db, type_mapping),
)),
Type::NominalInstance(instance) => Type::NominalInstance(
instance.apply_specialization(db, specialization),
instance.apply_type_mapping(db, type_mapping),
),
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(
function.apply_specialization(db, specialization),
function.apply_type_mapping(db, type_mapping),
))
}
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(
function.apply_specialization(db, specialization),
function.apply_type_mapping(db, type_mapping),
))
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => {
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(
property.apply_specialization(db, specialization),
property.apply_type_mapping(db, type_mapping),
))
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => {
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(
property.apply_specialization(db, specialization),
property.apply_type_mapping(db, type_mapping),
))
}
Type::Callable(callable) => {
Type::Callable(callable.apply_specialization(db, specialization))
Type::Callable(callable.apply_type_mapping(db, type_mapping))
}
Type::GenericAlias(generic) => {
let specialization = generic
.specialization(db)
.apply_specialization(db, specialization);
.apply_type_mapping(db, type_mapping);
Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization))
}
Type::PropertyInstance(property) => {
Type::PropertyInstance(property.apply_specialization(db, specialization))
Type::PropertyInstance(property.apply_type_mapping(db, type_mapping))
}
Type::Union(union) => union.map(db, |element| {
element.apply_specialization(db, specialization)
element.apply_type_mapping(db, type_mapping)
}),
Type::Intersection(intersection) => {
let mut builder = IntersectionBuilder::new(db);
for positive in intersection.positive(db) {
builder =
builder.add_positive(positive.apply_specialization(db, specialization));
builder.add_positive(positive.apply_type_mapping(db, type_mapping));
}
for negative in intersection.negative(db) {
builder =
builder.add_negative(negative.apply_specialization(db, specialization));
builder.add_negative(negative.apply_type_mapping(db, type_mapping));
}
builder.build()
}
@ -5119,7 +5127,7 @@ impl<'db> Type<'db> {
db,
tuple
.iter(db)
.map(|ty| ty.apply_specialization(db, specialization)),
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
),
Type::Dynamic(_)
@ -6844,6 +6852,10 @@ impl<'db> FunctionType<'db> {
)
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
self.apply_specialization(db, type_mapping.into_specialization(db))
}
fn find_legacy_typevars(
self,
db: &'db dyn Db,
@ -7192,15 +7204,12 @@ impl<'db> CallableType<'db> {
)
}
/// Apply a specialization to this callable type.
///
/// See [`Type::apply_specialization`] for more details.
fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
CallableType::from_overloads(
db,
self.signatures(db)
.iter()
.map(|signature| signature.apply_specialization(db, specialization)),
.map(|signature| signature.apply_type_mapping(db, type_mapping)),
)
}

View file

@ -1368,21 +1368,6 @@ impl<'db> Binding<'db> {
&self.parameter_tys
}
/// Returns the bound types for each parameter, in parameter source order, with default values
/// applied for arguments that weren't matched to a parameter. Returns `None` if there are any
/// non-default arguments that weren't matched to a parameter.
pub(crate) fn parameter_types_with_defaults(
&self,
signature: &Signature<'db>,
) -> Option<Box<[Type<'db>]>> {
signature
.parameters()
.iter()
.zip(&self.parameter_tys)
.map(|(parameter, parameter_ty)| parameter_ty.or(parameter.default_type()))
.collect()
}
pub(crate) fn arguments_for_parameter<'a>(
&'a self,
argument_types: &'a CallArgumentTypes<'a, 'db>,

View file

@ -8,7 +8,7 @@ use super::{
};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::DeclarationWithConstraint;
use crate::types::generics::{GenericContext, Specialization};
use crate::types::generics::{GenericContext, Specialization, TypeMapping};
use crate::types::signatures::{Parameter, Parameters};
use crate::types::{
CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature,
@ -147,16 +147,11 @@ impl<'db> GenericAlias<'db> {
self.origin(db).definition(db)
}
pub(super) fn apply_specialization(
self,
db: &'db dyn Db,
specialization: Specialization<'db>,
) -> Self {
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
Self::new(
db,
self.origin(db),
self.specialization(db)
.apply_specialization(db, specialization),
self.specialization(db).apply_type_mapping(db, type_mapping),
)
}
}
@ -236,16 +231,14 @@ impl<'db> ClassType<'db> {
self.is_known(db, KnownClass::Object)
}
pub(super) fn apply_specialization(
pub(super) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
specialization: Specialization<'db>,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
match self {
Self::NonGeneric(_) => self,
Self::Generic(generic) => {
Self::Generic(generic.apply_specialization(db, specialization))
}
Self::Generic(generic) => Self::Generic(generic.apply_type_mapping(db, type_mapping)),
}
}

View file

@ -1,4 +1,4 @@
use crate::types::generics::{GenericContext, Specialization};
use crate::types::generics::{GenericContext, Specialization, TypeMapping};
use crate::types::{
todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, MroIterator, Type,
};
@ -215,13 +215,9 @@ impl<'db> ClassBase<'db> {
}
}
pub(crate) fn apply_specialization(
self,
db: &'db dyn Db,
specialization: Specialization<'db>,
) -> Self {
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
match self {
Self::Class(class) => Self::Class(class.apply_specialization(db, specialization)),
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => self,
}
}
@ -232,7 +228,7 @@ impl<'db> ClassBase<'db> {
specialization: Option<Specialization<'db>>,
) -> Self {
if let Some(specialization) = specialization {
self.apply_specialization(db, specialization)
self.apply_type_mapping(db, specialization.type_mapping())
} else {
self
}

View file

@ -130,12 +130,7 @@ impl<'db> GenericContext<'db> {
}
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self
.variables(db)
.iter()
.map(|typevar| typevar.default_ty(db).unwrap_or(Type::unknown()))
.collect();
self.specialize(db, types)
self.specialize_partial(db, &vec![None; self.variables(db).len()])
}
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
@ -157,7 +152,9 @@ impl<'db> GenericContext<'db> {
}
/// Creates a specialization of this generic context. Panics if the length of `types` does not
/// match the number of typevars in the generic context.
/// match the number of typevars in the generic context. You must provide a specific type for
/// each typevar; no defaults are used. (Use [`specialize_partial`](Self::specialize_partial)
/// if you might not have types for every typevar.)
pub(crate) fn specialize(
self,
db: &'db dyn Db,
@ -166,6 +163,50 @@ impl<'db> GenericContext<'db> {
assert!(self.variables(db).len() == types.len());
Specialization::new(db, self, types)
}
/// Creates a specialization of this generic context. Panics if the length of `types` does not
/// match the number of typevars in the generic context. If any provided type is `None`, we
/// will use the corresponding typevar's default type.
pub(crate) fn specialize_partial(
self,
db: &'db dyn Db,
types: &[Option<Type<'db>>],
) -> Specialization<'db> {
let variables = self.variables(db);
assert!(variables.len() == types.len());
// Typevars can have other typevars as their default values, e.g.
//
// ```py
// class C[T, U = T]: ...
// ```
//
// If there is a mapping for `T`, we want to map `U` to that type, not to `T`. To handle
// this, we repeatedly apply the specialization to itself, until we reach a fixed point.
let mut expanded = vec![Type::unknown(); types.len()];
for (idx, (ty, typevar)) in types.iter().zip(variables).enumerate() {
if let Some(ty) = ty {
expanded[idx] = *ty;
continue;
}
let Some(default) = typevar.default_ty(db) else {
continue;
};
// Typevars are only allowed to refer to _earlier_ typevars in their defaults. (This is
// statically enforced for PEP-695 contexts, and is explicitly called out as a
// requirement for legacy contexts.)
let type_mapping = TypeMapping::Partial {
generic_context: self,
types: &expanded[0..idx],
};
let default = default.apply_type_mapping(db, type_mapping);
expanded[idx] = default;
}
Specialization::new(db, self, expanded.into_boxed_slice())
}
}
/// An assignment of a specific type to each type variable in a generic scope.
@ -180,6 +221,10 @@ pub struct Specialization<'db> {
}
impl<'db> Specialization<'db> {
pub(crate) fn type_mapping(self) -> TypeMapping<'db, 'db> {
TypeMapping::Specialization(self)
}
/// Applies a specialization to this specialization. This is used, for instance, when a generic
/// class inherits from a generic alias:
///
@ -194,10 +239,18 @@ impl<'db> Specialization<'db> {
/// That lets us produce the generic alias `A[int]`, which is the corresponding entry in the
/// MRO of `B[int]`.
pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self {
self.apply_type_mapping(db, other.type_mapping())
}
pub(crate) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
let types: Box<[_]> = self
.types(db)
.into_iter()
.map(|ty| ty.apply_specialization(db, other))
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect();
Specialization::new(db, self.generic_context(db), types)
}
@ -244,16 +297,6 @@ impl<'db> Specialization<'db> {
Self::new(db, self.generic_context(db), types)
}
/// Returns the type that a typevar is specialized to, or None if the typevar isn't part of
/// this specialization.
pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option<Type<'db>> {
let index = self
.generic_context(db)
.variables(db)
.get_index_of(&typevar)?;
Some(self.types(db)[index])
}
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
@ -403,6 +446,57 @@ impl<'db> Specialization<'db> {
}
}
/// A mapping between type variables and types.
///
/// You will usually use [`Specialization`] instead of this type. This type is used when we need to
/// substitute types for type variables before we have fully constructed a [`Specialization`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) enum TypeMapping<'a, 'db> {
Specialization(Specialization<'db>),
Partial {
generic_context: GenericContext<'db>,
types: &'a [Type<'db>],
},
}
impl<'db> TypeMapping<'_, 'db> {
fn generic_context(self, db: &'db dyn Db) -> GenericContext<'db> {
match self {
Self::Specialization(specialization) => specialization.generic_context(db),
Self::Partial {
generic_context, ..
} => generic_context,
}
}
/// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this
/// mapping.
pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option<Type<'db>> {
let index = self
.generic_context(db)
.variables(db)
.get_index_of(&typevar)?;
match self {
Self::Specialization(specialization) => specialization.types(db).get(index).copied(),
Self::Partial { types, .. } => types.get(index).copied(),
}
}
pub(crate) fn into_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
match self {
Self::Specialization(specialization) => specialization,
Self::Partial {
generic_context,
types,
} => {
let mut types = types.to_vec();
types.resize(generic_context.variables(db).len(), Type::unknown());
Specialization::new(db, generic_context, types.into_boxed_slice())
}
}
}
}
/// Performs type inference between parameter annotations and argument types, producing a
/// specialization of a generic function.
pub(crate) struct SpecializationBuilder<'db> {

View file

@ -6899,8 +6899,10 @@ impl<'db> TypeInferenceBuilder<'db> {
}
_ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]),
};
let signature = generic_context.signature(self.db());
let signatures = Signatures::single(CallableSignature::single(value_ty, signature.clone()));
let signatures = Signatures::single(CallableSignature::single(
value_ty,
generic_context.signature(self.db()),
));
let bindings = match Bindings::match_parameters(signatures, &call_argument_types)
.check_types(self.db(), &call_argument_types)
{
@ -6918,10 +6920,8 @@ impl<'db> TypeInferenceBuilder<'db> {
.matching_overloads()
.next()
.expect("valid bindings should have matching overload");
let parameters = overload
.parameter_types_with_defaults(&signature)
.expect("matching overload should not have missing arguments");
let specialization = generic_context.specialize(self.db(), parameters);
let specialization =
generic_context.specialize_partial(self.db(), overload.parameter_types());
Type::from(GenericAlias::new(self.db(), generic_class, specialization))
}

View file

@ -3,7 +3,7 @@
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type};
use crate::symbol::{Symbol, SymbolAndQualifiers};
use crate::types::generics::Specialization;
use crate::types::generics::TypeMapping;
use crate::Db;
pub(super) use synthesized_protocol::SynthesizedProtocolType;
@ -113,13 +113,13 @@ impl<'db> NominalInstanceType<'db> {
SubclassOfType::from(db, self.class)
}
pub(super) fn apply_specialization(
pub(super) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
specialization: Specialization<'db>,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
Self {
class: self.class.apply_specialization(db, specialization),
class: self.class.apply_type_mapping(db, type_mapping),
}
}
}

View file

@ -17,7 +17,7 @@ use smallvec::{smallvec, SmallVec};
use super::{definition_expression_type, DynamicType, Type};
use crate::semantic_index::definition::Definition;
use crate::types::generics::{GenericContext, Specialization};
use crate::types::generics::{GenericContext, Specialization, TypeMapping};
use crate::types::{todo_type, TypeVarInstance};
use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name};
@ -313,14 +313,22 @@ impl<'db> Signature<'db> {
&self,
db: &'db dyn Db,
specialization: Specialization<'db>,
) -> Self {
self.apply_type_mapping(db, specialization.type_mapping())
}
pub(crate) fn apply_type_mapping<'a>(
&self,
db: &'db dyn Db,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
parameters: self.parameters.apply_specialization(db, specialization),
parameters: self.parameters.apply_type_mapping(db, type_mapping),
return_ty: self
.return_ty
.map(|ty| ty.apply_specialization(db, specialization)),
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
}
}
@ -1053,12 +1061,12 @@ impl<'db> Parameters<'db> {
)
}
fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
Self {
value: self
.value
.iter()
.map(|param| param.apply_specialization(db, specialization))
.map(|param| param.apply_type_mapping(db, type_mapping))
.collect(),
is_gradual: self.is_gradual,
}
@ -1225,12 +1233,12 @@ impl<'db> Parameter<'db> {
self
}
fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
Self {
annotated_type: self
.annotated_type
.map(|ty| ty.apply_specialization(db, specialization)),
kind: self.kind.apply_specialization(db, specialization),
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
kind: self.kind.apply_type_mapping(db, type_mapping),
form: self.form,
}
}
@ -1422,24 +1430,24 @@ pub(crate) enum ParameterKind<'db> {
}
impl<'db> ParameterKind<'db> {
fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
match self {
Self::PositionalOnly { default_type, name } => Self::PositionalOnly {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_specialization(db, specialization)),
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
name: name.clone(),
},
Self::PositionalOrKeyword { default_type, name } => Self::PositionalOrKeyword {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_specialization(db, specialization)),
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
name: name.clone(),
},
Self::KeywordOnly { default_type, name } => Self::KeywordOnly {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_specialization(db, specialization)),
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
name: name.clone(),
},
Self::Variadic { .. } | Self::KeywordVariadic { .. } => self.clone(),