From b59c6d4fc43a2654aa68025d79601bfda546a61f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 21 Apr 2025 17:58:22 -0400 Subject: [PATCH] Try adding a specialization to non-generic classes --- .../resources/mdtest/generics/classes.md | 46 +++++++++++++++++-- crates/red_knot_python_semantic/src/types.rs | 10 ++-- .../src/types/class.rs | 25 +++++++++- .../src/types/infer.rs | 5 +- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index 6359978660..c85f2282bc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -56,6 +56,15 @@ class D(C[T]): ... (Examples `E` and `F` from above do not have analogues in the legacy syntax.) +## fwomp + +```py +class C[T]: + x: T + +reveal_type(C[int]()) # revealed: C[int] +``` + ## Specializing generic classes explicitly The type parameter can be specified explicitly: @@ -158,7 +167,7 @@ If the type of a constructor parameter is a class typevar, we can use that to in parameter. The types inferred from a type context and from a constructor parameter must be consistent with each other. -## `__new__` only +### `__new__` only ```py class C[T]: @@ -171,7 +180,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` -## `__init__` only +### `__init__` only ```py class C[T]: @@ -183,7 +192,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` -## Identical `__new__` and `__init__` signatures +### Identical `__new__` and `__init__` signatures ```py class C[T]: @@ -198,7 +207,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` -## Compatible `__new__` and `__init__` signatures +### Compatible `__new__` and `__init__` signatures ```py class C[T]: @@ -224,7 +233,7 @@ reveal_type(D(1)) # revealed: D[Literal[1]] wrong_innards: D[int] = D("five") ``` -## `__init__` is itself generic +### `__init__` is itself generic TODO: These do not currently work yet, because we don't correctly model the nested generic contexts. @@ -285,6 +294,33 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` +## Nested classes + +```py +class C[T]: + class D: + x: T + + class E[U]: + x: T + y: U + + def method1(self) -> "D": + return self.D() + + def method2(self) -> "E[str]": + return self.E[str]() + +reveal_type(C[int]().method1()) # revealed: D +# TODO: revealed: int +reveal_type(C[int]().method1().x) # revealed: T + +reveal_type(C[int]().method2()) # revealed: E[str] +# TODO: revealed: int +reveal_type(C[int]().method2().x) # revealed: T +reveal_type(C[int]().method2().y) # revealed: str +``` + ## Cyclic class definition A class can use itself as the type parameter of one of its superclasses. (This is also known as the diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b57443a5e4..ce15c4e469 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4651,6 +4651,12 @@ impl<'db> Type<'db> { Type::Callable(callable.apply_specialization(db, specialization)) } + Type::ClassLiteral(ClassLiteralType::NonGeneric(class)) => { + Type::from(class.apply_specialization(db, specialization)) + } + + Type::ClassLiteral(ClassLiteralType::Generic(_)) => self, + Type::GenericAlias(generic) => { let specialization = generic .specialization(db) @@ -4692,10 +4698,6 @@ impl<'db> Type<'db> { | Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) | Type::DataclassDecorator(_) | Type::ModuleLiteral(_) - // A non-generic class never needs to be specialized. A generic class is specialized - // explicitly (via a subscript expression) or implicitly (via a call), and not because - // some other generic context's specialization is applied to it. - | Type::ClassLiteral(_) // SubclassOf contains a ClassType, which has already been specialized if needed, like // above with BoundMethod's self_instance. | Type::SubclassOf(_) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 23a4a1fde6..0fce7dd5ad 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -134,6 +134,23 @@ impl<'db> Class<'db> { pub struct NonGenericClass<'db> { #[return_ref] pub(crate) class: Class<'db>, + + pub(crate) specialization: Option>, +} + +impl<'db> NonGenericClass<'db> { + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + eprintln!( + "==> specialize {} with {}", + Type::from(self).display(db), + specialization.display(db) + ); + NonGenericClass::new(db, self.class(db), Some(specialization)) + } } impl<'db> From> for Type<'db> { @@ -223,7 +240,13 @@ impl<'db> ClassType<'db> { fn specialize_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { - Self::NonGeneric(_) => ty, + Self::NonGeneric(non_generic) => { + if let Some(specialization) = non_generic.specialization(db) { + ty.apply_specialization(db, specialization) + } else { + ty + } + } Self::Generic(generic) => ty.apply_specialization(db, generic.specialization(db)), } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 466fe9be5a..b0673537bf 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1795,7 +1795,10 @@ impl<'db> TypeInferenceBuilder<'db> { Some(generic_context) => { ClassLiteralType::Generic(GenericClass::new(self.db(), class, generic_context)) } - None => ClassLiteralType::NonGeneric(NonGenericClass::new(self.db(), class)), + None => { + let specialization = None; + ClassLiteralType::NonGeneric(NonGenericClass::new(self.db(), class, specialization)) + } }; let class_ty = Type::from(class_literal);