From 3be8257126c11c84e34830726d8c479e9e907e88 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Mar 2025 13:13:45 -0400 Subject: [PATCH 001/100] Build context for generic classes --- crates/red_knot_python_semantic/src/types.rs | 1 + .../src/types/class.rs | 2 + .../src/types/generics.rs | 46 +++++++++++++++++++ .../src/types/infer.rs | 13 +++++- 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 crates/red_knot_python_semantic/src/types/generics.rs diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 7ecd3d42dacc9..116f3db09d6b0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -49,6 +49,7 @@ mod class_base; mod context; mod diagnostic; mod display; +mod generics; mod infer; mod mro; mod narrow; diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 85bf0a824e783..d3fcf252960cf 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -27,6 +27,7 @@ use super::{ KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType, TypeQualifiers, TypeVarInstance, }; +use crate::types::generics::GenericContext; /// Representation of a runtime class object. /// @@ -38,6 +39,7 @@ pub struct Class<'db> { #[return_ref] pub(crate) name: ast::name::Name, + generic_context: Option>, body_scope: ScopeId<'db>, pub(crate) known: Option, diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs new file mode 100644 index 0000000000000..5cf9faaef6606 --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -0,0 +1,46 @@ +use ruff_python_ast as ast; + +use crate::semantic_index::SemanticIndex; +use crate::types::{declaration_type, KnownInstanceType, Type, TypeVarInstance}; +use crate::Db; + +/// A list of formal type variables for a generic function, class, or type alias. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct GenericContext<'db> { + variables: Box<[TypeVarInstance<'db>]>, +} + +impl<'db> GenericContext<'db> { + pub(crate) fn from_type_params( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + type_params_node: &ast::TypeParams, + ) -> Self { + let variables = type_params_node + .iter() + .filter_map(|type_param| Self::variable_from_type_param(db, index, type_param)) + .collect(); + Self { variables } + } + + fn variable_from_type_param( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + type_param_node: &ast::TypeParam, + ) -> Option> { + match type_param_node { + ast::TypeParam::TypeVar(node) => { + let definition = index.expect_single_definition(node); + let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = + declaration_type(db, definition).inner_type() + else { + panic!("typevar should be inferred as a TypeVarInstance"); + }; + Some(typevar) + } + // TODO: Support these! + ast::TypeParam::ParamSpec(_) => None, + ast::TypeParam::TypeVarTuple(_) => None, + } + } +} diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index ae0a3f57e0c0b..6c54280350635 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -73,6 +73,7 @@ use crate::types::diagnostic::{ INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, }; +use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ @@ -1633,6 +1634,10 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_decorator(decorator); } + let generic_context = type_params.as_ref().map(|type_params| { + GenericContext::from_type_params(self.db(), self.index, type_params) + }); + let body_scope = self .index .node_scope(NodeWithScopeRef::Class(class_node)) @@ -1640,7 +1645,13 @@ impl<'db> TypeInferenceBuilder<'db> { let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name); - let class = Class::new(self.db(), &name.id, body_scope, maybe_known_class); + let class = Class::new( + self.db(), + &name.id, + generic_context, + body_scope, + maybe_known_class, + ); let class_ty = Type::class_literal(class); self.add_declaration_with_binding( From 4c76cb5d50a4ace3d056c340c9fbdf109a67499b Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Mar 2025 14:06:20 -0400 Subject: [PATCH 002/100] Handle explicit specialization before outputting lints --- .../resources/mdtest/generics/classes.md | 18 ------------ .../resources/mdtest/stubs/class.md | 2 -- .../src/types/class.rs | 2 +- .../src/types/infer.rs | 29 +++++++++++++------ 4 files changed, 21 insertions(+), 30 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 d6f78f1fed200..9d1c1e536403f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -13,8 +13,6 @@ class C[T]: ... A class that inherits from a generic class, and fills its type parameters with typevars, is generic: ```py -# TODO: no error -# error: [non-subscriptable] class D[U](C[U]): ... ``` @@ -22,8 +20,6 @@ A class that inherits from a generic class, but fills its type parameters with c _not_ generic: ```py -# TODO: no error -# error: [non-subscriptable] class E(C[int]): ... ``` @@ -65,9 +61,7 @@ The type parameter can be specified explicitly: class C[T]: x: T -# TODO: no error # TODO: revealed: C[int] -# error: [non-subscriptable] reveal_type(C[int]()) # revealed: C ``` @@ -131,16 +125,11 @@ propagate through: class Base[T]: x: T | None = None -# TODO: no error -# error: [non-subscriptable] class Sub[U](Base[U]): ... -# TODO: no error # TODO: revealed: int | None -# error: [non-subscriptable] reveal_type(Base[int].x) # revealed: T | None # TODO: revealed: int | None -# error: [non-subscriptable] reveal_type(Sub[int].x) # revealed: T | None ``` @@ -155,8 +144,6 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet ```pyi class Base[T]: ... -# TODO: no error -# error: [non-subscriptable] class Sub(Base[Sub]): ... reveal_type(Sub) # revealed: Literal[Sub] @@ -168,9 +155,6 @@ A similar case can work in a non-stub file, if forward references are stringifie ```py class Base[T]: ... - -# TODO: no error -# error: [non-subscriptable] class Sub(Base["Sub"]): ... reveal_type(Sub) # revealed: Literal[Sub] @@ -183,8 +167,6 @@ In a non-stub file, without stringified forward references, this raises a `NameE ```py class Base[T]: ... -# TODO: the unresolved-reference error is correct, the non-subscriptable is not -# error: [non-subscriptable] # error: [unresolved-reference] class Sub(Base[Sub]): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md index e5d4956db9040..bd971797386ad 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md @@ -8,8 +8,6 @@ In type stubs, classes can reference themselves in their base class definitions. ```pyi class Foo[T]: ... -# TODO: actually is subscriptable -# error: [non-subscriptable] class Bar(Foo[Bar]): ... reveal_type(Bar) # revealed: Literal[Bar] diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index d3fcf252960cf..a2ec7ab0e060d 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -39,7 +39,7 @@ pub struct Class<'db> { #[return_ref] pub(crate) name: ast::name::Name, - generic_context: Option>, + pub(crate) generic_context: Option>, body_scope: ScopeId<'db>, pub(crate) known: Option, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 6c54280350635..9cf6649716e9e 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -61,6 +61,7 @@ use crate::symbol::{ typing_extensions_symbol, Boundness, LookupError, }; use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError}; +use crate::types::class::{ClassLiteralType, MetaclassErrorKind}; use crate::types::diagnostic::{ report_implicit_return_type, report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, report_invalid_assignment, @@ -77,12 +78,11 @@ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType, - IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, - MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType, - Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, - TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, - UnionType, + todo_type, Class, DynamicType, FunctionType, InstanceType, IntersectionBuilder, + IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, Parameter, + ParameterForm, Parameters, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, + Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; use crate::types::{CallableType, GeneralCallableType, Signature}; use crate::unpack::{Unpack, UnpackPosition}; @@ -5800,9 +5800,16 @@ impl<'db> TypeInferenceBuilder<'db> { } } - if matches!(value_ty, Type::ClassLiteral(class_literal) if class_literal.class().is_known(self.db(), KnownClass::Type)) - { - return KnownClass::GenericAlias.to_instance(self.db()); + if let Type::ClassLiteral(ClassLiteralType { class }) = value_ty { + if class.is_known(self.db(), KnownClass::Type) { + return KnownClass::GenericAlias.to_instance(self.db()); + } + + if class.generic_context(self.db()).is_some() { + // TODO: specialize the generic class using these explicit type + // variable assignments + return value_ty; + } } report_non_subscriptable( @@ -5825,6 +5832,10 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: proper support for generic classes // For now, just infer `Sequence`, if we see something like `Sequence[str]`. This allows us // to look up attributes on generic base classes, even if we don't understand generics yet. + // Note that this isn't handled by the clause up above for generic classes + // that use legacy type variables and an explicit `Generic` base class. + // Once we handle legacy typevars, this special case will be removed in + // favor of the specialization logic above. value_ty } _ => Type::unknown(), From 8048604897cf070e6e361121ebc524a0e8dbd6e2 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 27 Mar 2025 16:12:57 -0400 Subject: [PATCH 003/100] Explicitly specialize classes --- .../resources/mdtest/generics/classes.md | 65 ++++++++++++++++++- crates/red_knot_python_semantic/src/types.rs | 4 ++ .../src/types/generics.rs | 37 ++++++++++- .../src/types/infer.rs | 46 ++++++++++++- 4 files changed, 148 insertions(+), 4 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 9d1c1e536403f..98b78eb77b662 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -53,7 +53,7 @@ class D(C[T]): ... (Examples `E` and `F` from above do not have analogues in the legacy syntax.) -## Inferring generic class parameters +## Specializing generic classes explicitly The type parameter can be specified explicitly: @@ -65,9 +65,72 @@ class C[T]: reveal_type(C[int]()) # revealed: C ``` +The specialization must match the generic types: + +```py +# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2" +reveal_type(C[int, int]()) # revealed: Unknown +``` + +If the type variable has an upper bound, the specialized type must satisfy that bound: + +```py +class Bounded[T: int]: ... +class BoundedByUnion[T: int | str]: ... +class IntSubclass(int): ... + +# TODO: revealed: Bounded[int] +reveal_type(Bounded[int]()) # revealed: Bounded + +# TODO: revealed: Bounded[IntSubclass] +reveal_type(Bounded[IntSubclass]()) # revealed: Bounded + +# error: [invalid-argument-type] "Object of type `str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`" +reveal_type(Bounded[str]()) # revealed: Unknown + +# error: [invalid-argument-type] "Object of type `int | str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`" +reveal_type(Bounded[int | str]()) # revealed: Unknown + +# TODO: revealed: BoundedByUnion[int] +reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion + +# TODO: revealed: BoundedByUnion[IntSubclass] +reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion + +# TODO: revealed: BoundedByUnion[str] +reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion + +# TODO: revealed: BoundedByUnion[int | str] +reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion +``` + +If the type variable is constrained, the specialized type must satisfy those constraints: + +```py +class Constrained[T: (int, str)]: ... + +# TODO: revealed: Constrained[int] +reveal_type(Constrained[int]()) # revealed: Constrained + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Unknown +reveal_type(Constrained[IntSubclass]()) # revealed: Constrained + +# TODO: revealed: Constrained[str] +reveal_type(Constrained[str]()) # revealed: Constrained + +# error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`" +reveal_type(Constrained[object]()) # revealed: Unknown +``` + +## Inferring generic class parameters + We can infer the type parameter from a type context: ```py +class C[T]: + x: T + c: C[int] = C() # TODO: revealed: C[int] reveal_type(c) # revealed: C diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 116f3db09d6b0..9847416fcd1e9 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5481,6 +5481,10 @@ impl<'db> TupleType<'db> { pub fn len(&self, db: &'db dyn Db) -> usize { self.elements(db).len() } + + pub fn iter(&self, db: &'db dyn Db) -> impl Iterator> + 'db + '_ { + self.elements(db).iter().copied() + } } // Make sure that the `Type` enum does not grow unexpectedly. diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 5cf9faaef6606..8d65a0010fdef 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -1,13 +1,19 @@ +use std::sync::Arc; + use ruff_python_ast as ast; use crate::semantic_index::SemanticIndex; -use crate::types::{declaration_type, KnownInstanceType, Type, TypeVarInstance}; +use crate::types::signatures::{Parameter, Parameters, Signature}; +use crate::types::{ + declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance, + UnionType, +}; use crate::Db; /// A list of formal type variables for a generic function, class, or type alias. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GenericContext<'db> { - variables: Box<[TypeVarInstance<'db>]>, + variables: Arc<[TypeVarInstance<'db>]>, } impl<'db> GenericContext<'db> { @@ -43,4 +49,31 @@ impl<'db> GenericContext<'db> { ast::TypeParam::TypeVarTuple(_) => None, } } + + pub(crate) fn signature(&self, db: &'db dyn Db) -> Signature<'db> { + let parameters = Parameters::new( + self.variables + .iter() + .map(|typevar| Self::parameter_from_typevar(db, *typevar)), + ); + Signature::new(parameters, None) + } + + fn parameter_from_typevar(db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Parameter<'db> { + let mut parameter = Parameter::positional_only(Some(typevar.name(db).clone())); + match typevar.bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + // TODO: This should be a type form. + parameter = parameter.with_annotated_type(bound); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // TODO: This should be a new type variant where only these exact types are + // assignable, and not subclasses of them. + parameter = parameter + .with_annotated_type(UnionType::from_elements(db, constraints.iter(db))); + } + None => {} + } + parameter + } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9cf6649716e9e..0bb2dde042d6c 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -84,7 +84,7 @@ use crate::types::{ Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; -use crate::types::{CallableType, GeneralCallableType, Signature}; +use crate::types::{CallableSignature, CallableType, GeneralCallableType, Signature, Signatures}; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; @@ -5568,11 +5568,55 @@ impl<'db> TypeInferenceBuilder<'db> { ctx: _, } = subscript; + // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the + // subscript inference logic and treat this as an explicit specialization. + // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return + // this callable as the `__class_getitem__` method on `type`. That probably requires + // updating all of the subscript logic below to use custom callables for all of the _other_ + // special cases, too. let value_ty = self.infer_expression(value); + if let Type::ClassLiteral(ClassLiteralType { class }) = value_ty { + if let Some(generic_context) = class.generic_context(self.db()) { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + &generic_context, + slice, + ); + } + } + let slice_ty = self.infer_expression(slice); self.infer_subscript_expression_types(value, value_ty, slice_ty) } + fn infer_explicit_class_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_context: &GenericContext<'db>, + slice_node: &ast::Expr, + ) -> Type<'db> { + let mut call_argument_types = match slice_node { + ast::Expr::Tuple(tuple) => CallArgumentTypes::positional( + tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), + ), + _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), + }; + let signatures = Signatures::single(CallableSignature::single( + value_ty, + generic_context.signature(self.db()), + )); + if let Err(CallError(_, bindings)) = + Bindings::match_parameters(signatures, &mut call_argument_types) + .check_types(self.db(), &mut call_argument_types) + { + bindings.report_diagnostics(&self.context, subscript.into()); + return Type::unknown(); + } + value_ty + } + fn infer_subscript_expression_types( &mut self, value_node: &ast::Expr, From fe76d56d2711f137de3283a358fc80267dc3e450 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 27 Mar 2025 16:32:24 -0400 Subject: [PATCH 004/100] Track those structs --- .../red_knot_python_semantic/src/types/generics.rs | 12 +++++------- crates/red_knot_python_semantic/src/types/infer.rs | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 8d65a0010fdef..d7a4ef40baced 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use ruff_python_ast as ast; use crate::semantic_index::SemanticIndex; @@ -11,9 +9,9 @@ use crate::types::{ use crate::Db; /// A list of formal type variables for a generic function, class, or type alias. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[salsa::tracked(debug)] pub struct GenericContext<'db> { - variables: Arc<[TypeVarInstance<'db>]>, + variables: Box<[TypeVarInstance<'db>]>, } impl<'db> GenericContext<'db> { @@ -26,7 +24,7 @@ impl<'db> GenericContext<'db> { .iter() .filter_map(|type_param| Self::variable_from_type_param(db, index, type_param)) .collect(); - Self { variables } + Self::new(db, variables) } fn variable_from_type_param( @@ -50,9 +48,9 @@ impl<'db> GenericContext<'db> { } } - pub(crate) fn signature(&self, db: &'db dyn Db) -> Signature<'db> { + pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { let parameters = Parameters::new( - self.variables + self.variables(db) .iter() .map(|typevar| Self::parameter_from_typevar(db, *typevar)), ); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 0bb2dde042d6c..b9a470ed89f98 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -5580,7 +5580,7 @@ impl<'db> TypeInferenceBuilder<'db> { return self.infer_explicit_class_specialization( subscript, value_ty, - &generic_context, + generic_context, slice, ); } @@ -5594,7 +5594,7 @@ impl<'db> TypeInferenceBuilder<'db> { &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, - generic_context: &GenericContext<'db>, + generic_context: GenericContext<'db>, slice_node: &ast::Expr, ) -> Type<'db> { let mut call_argument_types = match slice_node { From 4346e7d251d2db3662525a8f5fde1801709b0502 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 27 Mar 2025 16:34:25 -0400 Subject: [PATCH 005/100] Add Specialization --- .../src/types/generics.rs | 15 ++++++++++ .../src/types/infer.rs | 29 +++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index d7a4ef40baced..3826f1f4ee0b3 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -74,4 +74,19 @@ impl<'db> GenericContext<'db> { } parameter } + + pub(crate) fn specialize( + self, + db: &'db dyn Db, + types: Box<[Type<'db>]>, + ) -> Specialization<'db> { + Specialization::new(db, self, types) + } +} + +/// An assignment of a specific type to each type variable in a generic scope. +#[salsa::tracked(debug)] +pub(crate) struct Specialization<'db> { + generic_context: GenericContext<'db>, + types: Box<[Type<'db>]>, } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index b9a470ed89f98..681c15500a472 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -5607,13 +5607,30 @@ impl<'db> TypeInferenceBuilder<'db> { value_ty, generic_context.signature(self.db()), )); - if let Err(CallError(_, bindings)) = - Bindings::match_parameters(signatures, &mut call_argument_types) - .check_types(self.db(), &mut call_argument_types) + let bindings = match Bindings::match_parameters(signatures, &mut call_argument_types) + .check_types(self.db(), &mut call_argument_types) { - bindings.report_diagnostics(&self.context, subscript.into()); - return Type::unknown(); - } + Ok(bindings) => bindings, + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, subscript.into()); + return Type::unknown(); + } + }; + let callable = bindings + .into_iter() + .next() + .expect("valid bindings should have one callable"); + let (_, overload) = callable + .matching_overload() + .expect("valid bindings should have matching overload"); + let _specialization = generic_context.specialize( + self.db(), + overload + .parameter_types() + .iter() + .map(|ty| ty.unwrap_or(Type::unknown())) + .collect(), + ); value_ty } From 2380ade79d2e710fb157f0051405913b9b489ac4 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sun, 30 Mar 2025 11:59:51 -0400 Subject: [PATCH 006/100] Add CallableType::Specialized --- crates/red_knot_python_semantic/src/types.rs | 58 +++++++++++++++++++ .../src/types/display.rs | 7 +++ .../src/types/generics.rs | 2 +- .../src/types/type_ordering.rs | 7 +++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 9847416fcd1e9..8be15121753f5 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -35,6 +35,7 @@ use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; use crate::types::call::{Bindings, CallArgumentTypes}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; +use crate::types::generics::Specialization; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; @@ -641,6 +642,10 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), + (Type::Callable(CallableType::Specialized(specialized)), _) => { + specialized.callable_type(db).is_subtype_of(db, target) + } + // The same reasoning applies for these special callable types: (Type::Callable(CallableType::BoundMethod(_)), _) => KnownClass::MethodType .to_instance(db) @@ -1048,6 +1053,7 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(..) | Type::Callable( CallableType::BoundMethod(..) + | CallableType::Specialized(..) | CallableType::MethodWrapperDunderGet(..) | CallableType::WrapperDescriptorDunderGet, ) @@ -1062,6 +1068,7 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(..) | Type::Callable( CallableType::BoundMethod(..) + | CallableType::Specialized(..) | CallableType::MethodWrapperDunderGet(..) | CallableType::WrapperDescriptorDunderGet, ) @@ -1236,6 +1243,11 @@ impl<'db> Type<'db> { !KnownClass::FunctionType.is_subclass_of(db, class) } + (Type::Callable(CallableType::Specialized(specialized)), Type::Instance(_)) + | (Type::Instance(_), Type::Callable(CallableType::Specialized(specialized))) => { + specialized.callable_type(db).is_disjoint_from(db, other) + } + ( Type::Callable(CallableType::BoundMethod(_)), Type::Instance(InstanceType { class }), @@ -1359,6 +1371,9 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_fully_static(db)), Type::Callable(CallableType::General(callable)) => callable.is_fully_static(db), + Type::Callable(CallableType::Specialized(specialized)) => { + specialized.callable_type(db).is_fully_static(db) + } } } @@ -1398,6 +1413,9 @@ impl<'db> Type<'db> { // signature. false } + Type::Callable(CallableType::Specialized(specialized)) => { + specialized.callable_type(db).is_singleton(db) + } Type::Instance(InstanceType { class }) => { class.known(db).is_some_and(KnownClass::is_singleton) } @@ -1447,6 +1465,10 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, + Type::Callable(CallableType::Specialized(specialized)) => { + specialized.callable_type(db).is_single_valued(db) + } + Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` false @@ -1563,6 +1585,11 @@ impl<'db> Type<'db> { .find_name_in_mro(db, name) } + Type::Callable(CallableType::Specialized(specialized)) => { + // XXX: specialize the result + specialized.callable_type(db).find_name_in_mro(db, name) + } + Type::FunctionLiteral(_) | Type::Callable(_) | Type::ModuleLiteral(_) @@ -1638,6 +1665,10 @@ impl<'db> Type<'db> { Type::Callable(CallableType::BoundMethod(_)) => KnownClass::MethodType .to_instance(db) .instance_member(db, name), + Type::Callable(CallableType::Specialized(specialized)) => { + // XXX: specialize the result + specialized.callable_type(db).instance_member(db, name) + } Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { KnownClass::MethodWrapperType .to_instance(db) @@ -1992,6 +2023,12 @@ impl<'db> Type<'db> { }) } }, + Type::Callable(CallableType::Specialized(specialized)) => { + // XXX: specialize the result + specialized + .callable_type(db) + .member_lookup_with_policy(db, name, policy) + } Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { KnownClass::MethodWrapperType .to_instance(db) @@ -3168,6 +3205,10 @@ impl<'db> Type<'db> { Type::Callable(CallableType::BoundMethod(_)) => { KnownClass::MethodType.to_class_literal(db) } + Type::Callable(CallableType::Specialized(specialized)) => { + // XXX: specialize the result + specialized.callable_type(db).to_meta_type(db) + } Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { KnownClass::MethodWrapperType.to_class_literal(db) } @@ -4313,6 +4354,18 @@ pub struct BoundMethodType<'db> { self_instance: Type<'db>, } +/// Represents the specialization of a callable that has access to generic typevars, either because +/// it is itself a generic function, or because it appears in the body of a generic class. +#[salsa::tracked(debug)] +pub struct SpecializedCallable<'db> { + /// The callable that has been specialized. (Note that this is not [`CallableType`] since there + /// are other types that are callable.) + pub(crate) callable_type: Type<'db>, + + /// The specialization of any generic typevars that are visible to the callable. + pub(crate) specialization: Specialization<'db>, +} + /// This type represents a general callable type that are used to represent `typing.Callable` /// and `lambda` expressions. #[salsa::interned(debug)] @@ -4843,6 +4896,11 @@ pub enum CallableType<'db> { /// the bound instance when that type is displayed. BoundMethod(BoundMethodType<'db>), + /// Represents the specialization of a callable that has access to generic typevars, either + /// because it is itself a generic function, or because it appears in the body of a generic + /// class. + Specialized(SpecializedCallable<'db>), + /// Represents the callable `f.__get__` where `f` is a function. /// /// TODO: This could eventually be replaced by a more general `Callable` type that is diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 3d4fb84c8c6be..eb8e825e13527 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -100,6 +100,13 @@ impl Display for DisplayRepresentation<'_> { instance = bound_method.self_instance(self.db).display(self.db) ) } + Type::Callable(CallableType::Specialized(specialized)) => { + write!( + f, + "", + callable = specialized.callable_type(self.db).display(self.db), + ) + } Type::Callable(CallableType::MethodWrapperDunderGet(function)) => { write!( f, diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 3826f1f4ee0b3..503d998836889 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -86,7 +86,7 @@ impl<'db> GenericContext<'db> { /// An assignment of a specific type to each type variable in a generic scope. #[salsa::tracked(debug)] -pub(crate) struct Specialization<'db> { +pub struct Specialization<'db> { generic_context: GenericContext<'db>, types: Box<[Type<'db>]>, } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 79f0abe1d4f7a..3a0e39168cb36 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -69,6 +69,13 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::Callable(CallableType::BoundMethod(_)), _) => Ordering::Less, (_, Type::Callable(CallableType::BoundMethod(_))) => Ordering::Greater, + ( + Type::Callable(CallableType::Specialized(left)), + Type::Callable(CallableType::Specialized(right)), + ) => left.cmp(right), + (Type::Callable(CallableType::Specialized(_)), _) => Ordering::Less, + (_, Type::Callable(CallableType::Specialized(_))) => Ordering::Greater, + ( Type::Callable(CallableType::MethodWrapperDunderGet(left)), Type::Callable(CallableType::MethodWrapperDunderGet(right)), From 178ee89d950fc91b01fe8dcf2329478bd20d46f6 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 31 Mar 2025 17:04:57 -0400 Subject: [PATCH 007/100] Use union to hold typevar constraints --- crates/red_knot_python_semantic/src/types.rs | 2 +- crates/red_knot_python_semantic/src/types/infer.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d5ed2b6d777c6..a7e77346f558d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3496,7 +3496,7 @@ impl<'db> TypeVarInstance<'db> { #[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)] pub enum TypeVarBoundOrConstraints<'db> { UpperBound(Type<'db>), - Constraints(TupleType<'db>), + Constraints(UnionType<'db>), } /// Error returned if a type is not (or may not be) a context manager. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index abc2accc0b746..9e9f3d6ebf743 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1971,14 +1971,15 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_expression(expr); None } else { - let tuple = TupleType::new( + let union = UnionType::from_elements( self.db(), - elts.iter() - .map(|expr| self.infer_type_expression(expr)) - .collect::>(), + elts.iter().map(|expr| self.infer_type_expression(expr)), ); - let constraints = TypeVarBoundOrConstraints::Constraints(tuple); - self.store_expression_type(expr, Type::Tuple(tuple)); + let elements = union.into_union().expect( + "should have already checked that there were at least two constraints", + ); + let constraints = TypeVarBoundOrConstraints::Constraints(elements); + self.store_expression_type(expr, union); Some(constraints) } } From feb1ea9d237000810e980379b7aa5371374a91c2 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 31 Mar 2025 17:47:33 -0400 Subject: [PATCH 008/100] Add Type::TypeVar variant --- .../resources/mdtest/generics/functions.md | 3 - .../resources/mdtest/generics/pep695.md | 116 +++++++ crates/red_knot_python_semantic/src/types.rs | 310 ++++++++++++------ .../src/types/class_base.rs | 1 + .../src/types/display.rs | 3 + .../src/types/infer.rs | 3 + .../src/types/type_ordering.rs | 3 + 7 files changed, 331 insertions(+), 108 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index 712152ba3e5a6..55e61271e13db 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -108,7 +108,6 @@ def good_return[T: int](x: T) -> T: def bad_return[T: int](x: T) -> T: # TODO: error: int is not assignable to T - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `Literal[1]`" return x + 1 ``` @@ -138,8 +137,6 @@ methods that are compatible with the return type, so the `return` expression is ```py def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: - # TODO: no error - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`" return t1 + t2 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index af60aef39439c..06e9f8f563a03 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -48,4 +48,120 @@ class C[T]: reveal_type(x) # revealed: T ``` +## Subtyping and assignability + +An unbounded, unconstrained typevar is assignable to itself, but is not a subtype of itself (or any +other type), since it might be specialized to `Any`, which does not participate in the subtyping +relationship. + +It is neither assignable to or a subtype of any other type (including other typevars), since we can +make no assumption about what type it will be specialized to. + +```py +from knot_extensions import is_assignable_to, is_subtype_of, static_assert + +def unbounded_unconstrained[T, U](t: T, u: U) -> None: + static_assert(is_assignable_to(T, T)) + static_assert(is_assignable_to(U, U)) + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, T)) + static_assert(not is_subtype_of(U, U)) + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) +``` + +A bounded typevar is assignable to its bound, but the bound is not assignable to the typevar (since +the typevar might be specialized to a smaller type). (TODO: Unless the bound is final, in which case +the final class is also assignable to the typevar.) + +```py +from typing_extensions import final + +def bounded[T: int](t: T) -> None: + static_assert(is_assignable_to(T, int)) + static_assert(not is_assignable_to(int, T)) + + static_assert(not is_subtype_of(T, int)) + static_assert(not is_subtype_of(int, T)) + +@final +class FinalClass: ... + +def bounded_final[T: FinalClass](t: T) -> None: + static_assert(is_assignable_to(T, FinalClass)) + # TODO: is_assignable_to + static_assert(not is_assignable_to(FinalClass, T)) + + static_assert(not is_subtype_of(T, FinalClass)) + static_assert(not is_subtype_of(FinalClass, T)) +``` + +Two distinct typevars are not assignable to each other, even if they have the same bounds, since +there is (still) no guarantee that they will be specialized to the same type. (TODO: Unless the +bound is final, in which case we _can_ assume that the two typevars must always be specialized to +that final class.) + +```py +def two_bounded[T: int, U: int](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) + +def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: + # TODO: is_assignable_to + static_assert(not is_assignable_to(T, U)) + # TODO: is_assignable_to + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) +``` + +A constrained typevar is assignable to the union of its constraints, but not to any of the +constraints individually. None of the constraints are assignable to the typevar. + +```py +def constrained[T: (int, str)](t: T) -> None: + static_assert(not is_assignable_to(T, int)) + static_assert(not is_assignable_to(T, str)) + static_assert(is_assignable_to(T, int | str)) + static_assert(not is_assignable_to(int, T)) + static_assert(not is_assignable_to(str, T)) + static_assert(not is_assignable_to(int | str, T)) + + static_assert(not is_subtype_of(T, int)) + static_assert(not is_subtype_of(T, str)) + static_assert(not is_subtype_of(T, int | str)) + static_assert(not is_subtype_of(int, T)) + static_assert(not is_subtype_of(str, T)) + static_assert(not is_subtype_of(int | str, T)) +``` + +Two distinct typevars are not assignable to each other, even if they have the same constraints, and +even if any of the constraints are final. There must always be at least two distinct constraints, +meaning that there is (still) no guarantee that they will be specialized to the same type. + +```py +def two_constrainted[T: (int, str), U: (int, str)](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) + +@final +class AnotherFinalClass: ... + +def two_final_constrainted[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) +``` + [pep 695]: https://peps.python.org/pep-0695/ diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a7e77346f558d..b25d3eeae2bb8 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -273,7 +273,8 @@ pub enum Type<'db> { /// A heterogeneous tuple type, with elements of the given types in source order. // TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`. Tuple(TupleType<'db>), - // TODO protocols, callable types, overloads, generics, type vars + TypeVar(TypeVarInstance<'db>), + // TODO protocols, callable types, overloads, generics } #[salsa::tracked] @@ -515,7 +516,8 @@ impl<'db> Type<'db> { | Type::ClassLiteral(_) | Type::KnownInstance(_) | Type::IntLiteral(_) - | Type::SubclassOf(_) => self, + | Type::SubclassOf(_) + | Type::TypeVar(_) => self, } } @@ -550,7 +552,7 @@ impl<'db> Type<'db> { match (self, target) { // We should have handled these immediately above. - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { + (Type::Dynamic(_) | Type::TypeVar(_), _) | (_, Type::Dynamic(_) | Type::TypeVar(_)) => { unreachable!("Non-fully-static types do not participate in subtyping!") } @@ -754,6 +756,26 @@ impl<'db> Type<'db> { if self.is_gradual_equivalent_to(db, target) { return true; } + + // A typevar is assignable to its upper bound, and to something similar to the union of + // its constraints. An unbound, unconstrained typevar is assignable to any type. + // TODO: It's not _really_ the union of its constraints because each occurrence of the + // typevar must be bound to the same type. + if let Type::TypeVar(typevar) = self { + match typevar.bound_or_constraints(db) { + None => {} + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + return bound.is_assignable_to(db, target); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + return constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_assignable_to(db, target)); + } + } + } + match (self, target) { // Never can be assigned to any type. (Type::Never, _) => true, @@ -804,6 +826,24 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), + // A typevar is always assignable to itself, and is never assignable to any other + // typevar (since there is no guarantee that they will be specialized to the same + // type). + // TODO: Two distinct typevars that are both bounded by the same final class are + // subtypes of each other. + (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { + self_typevar == other_typevar + } + + // No types are assignable to a typevar, since there's no guarantee what type the + // typevar will be specialized to. (If the typevar is bounded, it might be specialized + // to a smaller type than the bound. If it is constrained, there must be multiple + // constraints, and the typevar might be specialized to any one of them.) + // TODO: If the typevar is bounded by a final type, then that final type _is_ + // assignable to the typevar, since that's the only fully static type it could be + // specialized to. + (_, Type::TypeVar(_)) => false, + // A tuple type S is assignable to a tuple type T if their lengths are the same, and // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { @@ -979,6 +1019,8 @@ impl<'db> Type<'db> { } } + (Type::TypeVar(first), Type::TypeVar(second)) => first == second, + (Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second), (Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second), @@ -1006,6 +1048,10 @@ impl<'db> Type<'db> { (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false, + // A typevar is never disjoint from any other type, since it might be specialized to + // `Any`. + (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => false, + (Type::Union(union), other) | (other, Type::Union(union)) => union .elements(db) .iter() @@ -1313,6 +1359,8 @@ impl<'db> Type<'db> { pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { match self { Type::Dynamic(_) => false, + // A typevar is not fully static, since it can be specialized to a generic type + Type::TypeVar(_) => false, Type::Never | Type::FunctionLiteral(..) | Type::Callable( @@ -1381,6 +1429,12 @@ impl<'db> Type<'db> { // are both of type Literal[345], for example. false } + + // A typevar is never a singleton, since it can always be specialized to `Any`. + // QUESTION: Should a bounded typevar be considered a singleton if its bound is? It can + // still be specialized to `Any`, instead of to the singleton bound. + Type::TypeVar(_) => false, + // We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton. Type::SubclassOf(..) => false, Type::BooleanLiteral(_) @@ -1448,6 +1502,11 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, + // A typevar is never single-valued, since it can always be specialized to `Any`. + // QUESTION: Should a bounded typevar be considered single-valued if its bound is? It + // can still be specialized to `Any`, instead of to the single-valued bound. + Type::TypeVar(_) => false, + Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` false @@ -1577,6 +1636,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::Tuple(_) + | Type::TypeVar(_) | Type::Instance(_) => None, } } @@ -1653,6 +1713,17 @@ impl<'db> Type<'db> { KnownClass::Object.to_instance(db).instance_member(db, name) } + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => KnownClass::Object.to_instance(db).instance_member(db, name), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.instance_member(db, name) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .map_with_boundness_and_qualifiers(db, |constraint| { + constraint.instance_member(db, name) + }), + }, + Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name), Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db).instance_member(db, name), Type::StringLiteral(_) | Type::LiteralString => { @@ -2058,6 +2129,7 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::SliceLiteral(..) | Type::Tuple(..) + | Type::TypeVar(..) | Type::KnownInstance(..) | Type::FunctionLiteral(..) => { let fallback = self.instance_member(db, name_str); @@ -2176,6 +2248,108 @@ impl<'db> Type<'db> { db: &'db dyn Db, allow_short_circuit: bool, ) -> Result> { + let type_to_truthiness = |ty| { + if let Type::BooleanLiteral(bool_val) = ty { + Truthiness::from(bool_val) + } else { + Truthiness::Ambiguous + } + }; + + let try_dunder_bool = || { + // We only check the `__bool__` method for truth testing, even though at + // runtime there is a fallback to `__len__`, since `__bool__` takes precedence + // and a subclass could add a `__bool__` method. + + match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) { + Ok(outcome) => { + let return_type = outcome.return_type(db); + if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + // The type has a `__bool__` method, but it doesn't return a + // boolean. + return Err(BoolError::IncorrectReturnType { + return_type, + not_boolable_type: *self, + }); + } + Ok(type_to_truthiness(return_type)) + } + + Err(CallDunderError::PossiblyUnbound(outcome)) => { + let return_type = outcome.return_type(db); + if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + // The type has a `__bool__` method, but it doesn't return a + // boolean. + return Err(BoolError::IncorrectReturnType { + return_type: outcome.return_type(db), + not_boolable_type: *self, + }); + } + + // Don't trust possibly unbound `__bool__` method. + Ok(Truthiness::Ambiguous) + } + + Err(CallDunderError::MethodNotAvailable) => Ok(Truthiness::Ambiguous), + Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => { + Err(BoolError::IncorrectArguments { + truthiness: type_to_truthiness(bindings.return_type(db)), + not_boolable_type: *self, + }) + } + Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => { + Err(BoolError::NotCallable { + not_boolable_type: *self, + }) + } + Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => { + Err(BoolError::Other { + not_boolable_type: *self, + }) + } + } + }; + + let try_union = |union: UnionType<'db>| { + let mut truthiness = None; + let mut all_not_callable = true; + let mut has_errors = false; + + for element in union.elements(db) { + let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) { + Ok(truthiness) => truthiness, + Err(err) => { + has_errors = true; + all_not_callable &= matches!(err, BoolError::NotCallable { .. }); + err.fallback_truthiness() + } + }; + + truthiness.get_or_insert(element_truthiness); + + if Some(element_truthiness) != truthiness { + truthiness = Some(Truthiness::Ambiguous); + + if allow_short_circuit { + return Ok(Truthiness::Ambiguous); + } + } + } + + if has_errors { + if all_not_callable { + return Err(BoolError::NotCallable { + not_boolable_type: *self, + }); + } + return Err(BoolError::Union { + union, + truthiness: truthiness.unwrap_or(Truthiness::Ambiguous), + }); + } + Ok(truthiness.unwrap_or(Truthiness::Ambiguous)) + }; + let truthiness = match self { Type::Dynamic(_) | Type::Never => Truthiness::Ambiguous, Type::FunctionLiteral(_) => Truthiness::AlwaysTrue, @@ -2192,110 +2366,23 @@ impl<'db> Type<'db> { }, Type::AlwaysTruthy => Truthiness::AlwaysTrue, Type::AlwaysFalsy => Truthiness::AlwaysFalse, - instance_ty @ Type::Instance(InstanceType { class }) => match class.known(db) { - Some(known_class) => known_class.bool(), - None => { - // We only check the `__bool__` method for truth testing, even though at - // runtime there is a fallback to `__len__`, since `__bool__` takes precedence - // and a subclass could add a `__bool__` method. - - let type_to_truthiness = |ty| { - if let Type::BooleanLiteral(bool_val) = ty { - Truthiness::from(bool_val) - } else { - Truthiness::Ambiguous - } - }; - match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) { - Ok(outcome) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { - // The type has a `__bool__` method, but it doesn't return a - // boolean. - return Err(BoolError::IncorrectReturnType { - return_type, - not_boolable_type: *instance_ty, - }); - } - type_to_truthiness(return_type) - } - - Err(CallDunderError::PossiblyUnbound(outcome)) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { - // The type has a `__bool__` method, but it doesn't return a - // boolean. - return Err(BoolError::IncorrectReturnType { - return_type: outcome.return_type(db), - not_boolable_type: *instance_ty, - }); - } - - // Don't trust possibly unbound `__bool__` method. - Truthiness::Ambiguous - } - - Err(CallDunderError::MethodNotAvailable) => Truthiness::Ambiguous, - Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => { - return Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(bindings.return_type(db)), - not_boolable_type: *instance_ty, - }); - } - Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => { - return Err(BoolError::NotCallable { - not_boolable_type: *instance_ty, - }); - } - Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => { - return Err(BoolError::Other { - not_boolable_type: *self, - }) - } - } + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => Truthiness::Ambiguous, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.try_bool_impl(db, allow_short_circuit)? } - }, - Type::KnownInstance(known_instance) => known_instance.bool(), - Type::Union(union) => { - let mut truthiness = None; - let mut all_not_callable = true; - let mut has_errors = false; - - for element in union.elements(db) { - let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) { - Ok(truthiness) => truthiness, - Err(err) => { - has_errors = true; - all_not_callable &= matches!(err, BoolError::NotCallable { .. }); - err.fallback_truthiness() - } - }; - - truthiness.get_or_insert(element_truthiness); - - if Some(element_truthiness) != truthiness { - truthiness = Some(Truthiness::Ambiguous); - - if allow_short_circuit { - return Ok(Truthiness::Ambiguous); - } - } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + try_union(constraints)? } + }, - if has_errors { - if all_not_callable { - return Err(BoolError::NotCallable { - not_boolable_type: *self, - }); - } - return Err(BoolError::Union { - union: *union, - truthiness: truthiness.unwrap_or(Truthiness::Ambiguous), - }); - } - truthiness.unwrap_or(Truthiness::Ambiguous) - } + Type::Instance(InstanceType { class }) => match class.known(db) { + Some(known_class) => known_class.bool(), + None => try_dunder_bool()?, + }, + Type::KnownInstance(known_instance) => known_instance.bool(), + Type::Union(union) => try_union(*union)?, Type::Intersection(_) => { // TODO Truthiness::Ambiguous @@ -2915,6 +3002,7 @@ impl<'db> Type<'db> { | Type::StringLiteral(_) | Type::SliceLiteral(_) | Type::Tuple(_) + | Type::TypeVar(_) | Type::LiteralString | Type::AlwaysTruthy | Type::AlwaysFalsy => None, @@ -2967,6 +3055,7 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::StringLiteral(_) | Type::Tuple(_) + | Type::TypeVar(_) | Type::Callable(_) | Type::Never | Type::FunctionLiteral(_) => Err(InvalidTypeExpressionError { @@ -2998,8 +3087,7 @@ impl<'db> Type<'db> { KnownInstanceType::Deque => Ok(KnownClass::Deque.to_instance(db)), KnownInstanceType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), - // TODO map this to a new `Type::TypeVar` variant - KnownInstanceType::TypeVar(_) => Ok(*self), + KnownInstanceType::TypeVar(typevar) => Ok(Type::TypeVar(*typevar)), // TODO: Use an opt-in rule for a bare `Callable` KnownInstanceType::Callable => Ok(Type::Callable(CallableType::General( @@ -3184,6 +3272,18 @@ impl<'db> Type<'db> { Type::Callable(CallableType::General(_)) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), + + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => KnownClass::Object.to_class_literal(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // TODO: This should create ClassLiterals, not SubclassOfTypes, for each + // element, since constraints must be specialized to those specific types, not + // to subclasses of those types. + constraints.map(db, |constraint| constraint.to_meta_type(db)) + } + }, + Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { ClassBase::Dynamic(_) => *self, diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 2d4161dc33508..c73ccb2f87e90 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -77,6 +77,7 @@ impl<'db> ClassBase<'db> { | Type::SliceLiteral(_) | Type::ModuleLiteral(_) | Type::SubclassOf(_) + | Type::TypeVar(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => None, Type::KnownInstance(known_instance) => match known_instance { diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 3d4fb84c8c6be..c9912a2788b56 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -154,6 +154,9 @@ impl Display for DisplayRepresentation<'_> { } f.write_str("]") } + Type::TypeVar(typevar) => { + write!(f, "{}", typevar.name(self.db)) + } Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9e9f3d6ebf743..de270678a5383 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2314,6 +2314,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::KnownInstance(..) | Type::FunctionLiteral(..) | Type::Callable(..) + | Type::TypeVar(..) | Type::AlwaysTruthy | Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) { meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { @@ -4372,6 +4373,7 @@ impl<'db> TypeInferenceBuilder<'db> { match (op, operand_type) { (_, Type::Dynamic(_)) => operand_type, (_, Type::Never) => Type::Never, + (_, Type::TypeVar(_)) => Type::unknown(), (ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), (ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), @@ -4514,6 +4516,7 @@ impl<'db> TypeInferenceBuilder<'db> { (todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _) | (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), + (Type::TypeVar(_), _, _) | (_, Type::TypeVar(_), _) => Some(Type::unknown()), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( n.checked_add(m) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 610c535663c4d..3e389073814eb 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -129,6 +129,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::Instance(_), _) => Ordering::Less, (_, Type::Instance(_)) => Ordering::Greater, + (Type::TypeVar(_), _) => Ordering::Less, + (_, Type::TypeVar(_)) => Ordering::Greater, + (Type::AlwaysTruthy, _) => Ordering::Less, (_, Type::AlwaysTruthy) => Ordering::Greater, From 3c1ad797b49f62184d5963efaf1dc1b7d74df601 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 31 Mar 2025 18:01:23 -0400 Subject: [PATCH 009/100] Fix failing tests --- crates/red_knot_python_semantic/src/types/infer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index de270678a5383..c91a2e95b40a8 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1975,9 +1975,9 @@ impl<'db> TypeInferenceBuilder<'db> { self.db(), elts.iter().map(|expr| self.infer_type_expression(expr)), ); - let elements = union.into_union().expect( - "should have already checked that there were at least two constraints", - ); + let elements = union + .into_union() + .unwrap_or_else(|| UnionType::new(self.db(), Box::from([union]))); let constraints = TypeVarBoundOrConstraints::Constraints(elements); self.store_expression_type(expr, union); Some(constraints) From 59430b6450abe6b634d153472873980155443012 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 09:31:36 -0400 Subject: [PATCH 010/100] doc Type::TypeVar --- crates/red_knot_python_semantic/src/types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b25d3eeae2bb8..e298de8d3ee7d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -273,6 +273,8 @@ pub enum Type<'db> { /// A heterogeneous tuple type, with elements of the given types in source order. // TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`. Tuple(TupleType<'db>), + /// An instance of a typevar in a generic class or function. When the generic class or function + /// is specialized, we will replace this typevar with its specialization. TypeVar(TypeVarInstance<'db>), // TODO protocols, callable types, overloads, generics } From 77ffa80daddfd6fe606cd93ad72371109d228662 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 10:24:47 -0400 Subject: [PATCH 011/100] More correct handling of final bounds/constraints --- .../resources/mdtest/generics/pep695.md | 56 ++++++++++++++--- crates/red_knot_python_semantic/src/types.rs | 61 ++++++++++++------- .../src/types/infer.rs | 20 ++++-- 3 files changed, 100 insertions(+), 37 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 06e9f8f563a03..5a9cfe604d9a7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -72,9 +72,9 @@ def unbounded_unconstrained[T, U](t: T, u: U) -> None: static_assert(not is_subtype_of(U, T)) ``` -A bounded typevar is assignable to its bound, but the bound is not assignable to the typevar (since -the typevar might be specialized to a smaller type). (TODO: Unless the bound is final, in which case -the final class is also assignable to the typevar.) +A bounded typevar is assignable to its bound, but the bound is not assignable to the typevar, since +the typevar might be specialized to a smaller type. (This is true even if the bound is a final +class, since the typevar can still be specialized to `Never`.) ```py from typing_extensions import final @@ -91,7 +91,6 @@ class FinalClass: ... def bounded_final[T: FinalClass](t: T) -> None: static_assert(is_assignable_to(T, FinalClass)) - # TODO: is_assignable_to static_assert(not is_assignable_to(FinalClass, T)) static_assert(not is_subtype_of(T, FinalClass)) @@ -99,9 +98,9 @@ def bounded_final[T: FinalClass](t: T) -> None: ``` Two distinct typevars are not assignable to each other, even if they have the same bounds, since -there is (still) no guarantee that they will be specialized to the same type. (TODO: Unless the -bound is final, in which case we _can_ assume that the two typevars must always be specialized to -that final class.) +there is (still) no guarantee that they will be specialized to the same type. This is true even if +both typevars are bounded by the same final class, since you can specialize the typevars to `Never` +in addition to that final class. ```py def two_bounded[T: int, U: int](t: T, u: U) -> None: @@ -112,9 +111,7 @@ def two_bounded[T: int, U: int](t: T, u: U) -> None: static_assert(not is_subtype_of(U, T)) def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: - # TODO: is_assignable_to static_assert(not is_assignable_to(T, U)) - # TODO: is_assignable_to static_assert(not is_assignable_to(U, T)) static_assert(not is_subtype_of(T, U)) @@ -164,4 +161,45 @@ def two_final_constrainted[T: (FinalClass, AnotherFinalClass), U: (FinalClass, A static_assert(not is_subtype_of(U, T)) ``` +## Singletons and single-valued types + +(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the +claims apply to _single-valued_ types as well.) + +An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a +non-singleton type. + +```py +from knot_extensions import is_singleton, is_single_valued, static_assert + +def unbounded_unconstrained[T](t: T) -> None: + static_assert(not is_singleton(T)) + static_assert(not is_single_valued(T)) +``` + +A bounded typevar is not a singleton, since it can still be specialized to `Never`. + +```py +def bounded[T: int](t: T) -> None: + static_assert(not is_singleton(T)) + static_assert(not is_single_valued(T)) +``` + +A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot +specialize a constrained typevar to a subtype of a constraint.) + +```py +from typing_extensions import Literal + +def constrained_non_singletons[T: (int, str)](t: T) -> None: + static_assert(not is_singleton(T)) + static_assert(not is_single_valued(T)) + +def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None: + static_assert(is_singleton(T)) + +def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: + static_assert(is_single_valued(T)) +``` + [pep 695]: https://peps.python.org/pep-0695/ diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e298de8d3ee7d..baddbfbed929a 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -829,21 +829,19 @@ impl<'db> Type<'db> { .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), // A typevar is always assignable to itself, and is never assignable to any other - // typevar (since there is no guarantee that they will be specialized to the same - // type). - // TODO: Two distinct typevars that are both bounded by the same final class are - // subtypes of each other. + // typevar, since there is no guarantee that they will be specialized to the same + // type. (This is true even if both typevars are bounded by the same final class, since + // you can specialize the typevars to `Never` in addition to that final class.) (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { self_typevar == other_typevar } // No types are assignable to a typevar, since there's no guarantee what type the - // typevar will be specialized to. (If the typevar is bounded, it might be specialized - // to a smaller type than the bound. If it is constrained, there must be multiple - // constraints, and the typevar might be specialized to any one of them.) - // TODO: If the typevar is bounded by a final type, then that final type _is_ - // assignable to the typevar, since that's the only fully static type it could be - // specialized to. + // typevar will be specialized to. If the typevar is bounded, it might be specialized + // to a smaller type than the bound. (This is true even if the bound is a final class, + // since the typevar can still be specialized to `Never`.) If it is constrained, there + // must be multiple constraints, and the typevar might be specialized to any one of + // them. (_, Type::TypeVar(_)) => false, // A tuple type S is assignable to a tuple type T if their lengths are the same, and @@ -1432,10 +1430,19 @@ impl<'db> Type<'db> { false } - // A typevar is never a singleton, since it can always be specialized to `Any`. - // QUESTION: Should a bounded typevar be considered a singleton if its bound is? It can - // still be specialized to `Any`, instead of to the singleton bound. - Type::TypeVar(_) => false, + // An unbounded, unconstrained typevar is not a singleton, because it can be + // specialized to a non-singleton type. A bounded typevar is not a singleton, even if + // the bound is a final singleton class, since it can still be specialized to `Never`. + // A constrained typevar is a singleton if all of its constraints are singletons. (Note + // that you cannot specialize a constrained typevar to a subtype of a constraint.) + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_singleton(db)), + }, // We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton. Type::SubclassOf(..) => false, @@ -1504,10 +1511,20 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, - // A typevar is never single-valued, since it can always be specialized to `Any`. - // QUESTION: Should a bounded typevar be considered single-valued if its bound is? It - // can still be specialized to `Any`, instead of to the single-valued bound. - Type::TypeVar(_) => false, + // An unbounded, unconstrained typevar is not single-valued, because it can be + // specialized to a multple-valued type. A bounded typevar is not single-valued, even + // if the bound is a final single-valued class, since it can still be specialized to + // `Never`. A constrained typevar is single-valued if all of its constraints are + // single-valued. (Note that you cannot specialize a constrained typevar to a subtype + // of a constraint.) + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_single_valued(db)), + }, Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` @@ -3554,10 +3571,10 @@ impl<'db> InvalidTypeExpression<'db> { /// Data regarding a single type variable. /// /// This is referenced by `KnownInstanceType::TypeVar` (to represent the singleton type of the -/// runtime `typing.TypeVar` object itself). In the future, it will also be referenced also by a -/// new `Type` variant to represent the type that this typevar represents as an annotation: that -/// is, an unknown set of objects, constrained by the upper-bound/constraints on this type var, -/// defaulting to the default type of this type var when not otherwise bound to a type. +/// runtime `typing.TypeVar` object itself), and by `Type::TypeVar` to represent the type that this +/// typevar represents as an annotation: that is, an unknown set of objects, constrained by the +/// upper-bound/constraints on this type var, defaulting to the default type of this type var when +/// not otherwise bound to a type. /// /// This must be a tracked struct, not an interned one, because typevar equivalence is by identity, /// not by value. Two typevars that have the same name, bound/constraints, and default, are still diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index c91a2e95b40a8..6df4a5b79f8df 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1971,15 +1971,23 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_expression(expr); None } else { - let union = UnionType::from_elements( + // We don't use UnionType::from_elements or UnionBuilder here, because we don't + // want to simplify the list of constraints like we do with the elements of an + // actual union type. + let elements = UnionType::new( self.db(), - elts.iter().map(|expr| self.infer_type_expression(expr)), + elts.iter() + .map(|expr| self.infer_type_expression(expr)) + .collect::>(), ); - let elements = union - .into_union() - .unwrap_or_else(|| UnionType::new(self.db(), Box::from([union]))); let constraints = TypeVarBoundOrConstraints::Constraints(elements); - self.store_expression_type(expr, union); + // But when we construct an actual union type for the constraint expression as + // a whole, we do use UnionType::from_elements to maintain the invariant that + // all union types are simplified. + self.store_expression_type( + expr, + UnionType::from_elements(self.db(), elements.elements(self.db())), + ); Some(constraints) } } From 6f8672041cde477228404da9b4e2484fd15a0056 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 10:27:02 -0400 Subject: [PATCH 012/100] use list[T] so generic funcs are callable even with Never --- .../resources/mdtest/generics/pep695.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 5a9cfe604d9a7..db4e5b621e37b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -60,7 +60,7 @@ make no assumption about what type it will be specialized to. ```py from knot_extensions import is_assignable_to, is_subtype_of, static_assert -def unbounded_unconstrained[T, U](t: T, u: U) -> None: +def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: static_assert(is_assignable_to(T, T)) static_assert(is_assignable_to(U, U)) static_assert(not is_assignable_to(T, U)) @@ -79,7 +79,7 @@ class, since the typevar can still be specialized to `Never`.) ```py from typing_extensions import final -def bounded[T: int](t: T) -> None: +def bounded[T: int](t: list[T]) -> None: static_assert(is_assignable_to(T, int)) static_assert(not is_assignable_to(int, T)) @@ -89,7 +89,7 @@ def bounded[T: int](t: T) -> None: @final class FinalClass: ... -def bounded_final[T: FinalClass](t: T) -> None: +def bounded_final[T: FinalClass](t: list[T]) -> None: static_assert(is_assignable_to(T, FinalClass)) static_assert(not is_assignable_to(FinalClass, T)) @@ -103,14 +103,14 @@ both typevars are bounded by the same final class, since you can specialize the in addition to that final class. ```py -def two_bounded[T: int, U: int](t: T, u: U) -> None: +def two_bounded[T: int, U: int](t: list[T], u: list[U]) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) -def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: +def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -122,7 +122,7 @@ A constrained typevar is assignable to the union of its constraints, but not to constraints individually. None of the constraints are assignable to the typevar. ```py -def constrained[T: (int, str)](t: T) -> None: +def constrained[T: (int, str)](t: list[T]) -> None: static_assert(not is_assignable_to(T, int)) static_assert(not is_assignable_to(T, str)) static_assert(is_assignable_to(T, int | str)) @@ -143,7 +143,7 @@ even if any of the constraints are final. There must always be at least two dist meaning that there is (still) no guarantee that they will be specialized to the same type. ```py -def two_constrainted[T: (int, str), U: (int, str)](t: T, u: U) -> None: +def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -153,7 +153,7 @@ def two_constrainted[T: (int, str), U: (int, str)](t: T, u: U) -> None: @final class AnotherFinalClass: ... -def two_final_constrainted[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: +def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -172,7 +172,7 @@ non-singleton type. ```py from knot_extensions import is_singleton, is_single_valued, static_assert -def unbounded_unconstrained[T](t: T) -> None: +def unbounded_unconstrained[T](t: list[T]) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) ``` @@ -180,7 +180,7 @@ def unbounded_unconstrained[T](t: T) -> None: A bounded typevar is not a singleton, since it can still be specialized to `Never`. ```py -def bounded[T: int](t: T) -> None: +def bounded[T: int](t: list[T]) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) ``` @@ -191,14 +191,14 @@ specialize a constrained typevar to a subtype of a constraint.) ```py from typing_extensions import Literal -def constrained_non_singletons[T: (int, str)](t: T) -> None: +def constrained_non_singletons[T: (int, str)](t: list[T]) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) -def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None: +def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None: static_assert(is_singleton(T)) -def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: +def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None: static_assert(is_single_valued(T)) ``` From cf819679376816a39d94a1ffdde29d4ee052997d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 10:35:21 -0400 Subject: [PATCH 013/100] lint --- crates/red_knot_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index baddbfbed929a..94984a46aaf77 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1512,7 +1512,7 @@ impl<'db> Type<'db> { | Type::KnownInstance(..) => true, // An unbounded, unconstrained typevar is not single-valued, because it can be - // specialized to a multple-valued type. A bounded typevar is not single-valued, even + // specialized to a multiple-valued type. A bounded typevar is not single-valued, even // if the bound is a final single-valued class, since it can still be specialized to // `Never`. A constrained typevar is single-valued if all of its constraints are // single-valued. (Note that you cannot specialize a constrained typevar to a subtype From 6615df1842bf86d3c1375ea225bf53b215d4a6ae Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 11:49:46 -0400 Subject: [PATCH 014/100] Add (currently failing) narrowing tests --- .../resources/mdtest/generics/pep695.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index db4e5b621e37b..00d3adc6e93a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -202,4 +202,28 @@ def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None static_assert(is_single_valued(T)) ``` +## Narrowing + +We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar: + +```py +class P: ... +class Q: ... + +def f[T: (P, Q)](t: T) -> None: + if isinstance(t, P): + # TODO: revealed: Q + reveal_type(t) # revealed: T & P + else: + # TODO: revealed: P + reveal_type(t) # revealed: T & ~P + + if isinstance(t, Q): + # TODO: revealed: P + reveal_type(t) # revealed: T & Q + else: + # TODO: revealed: Q + reveal_type(t) # revealed: T & ~Q +``` + [pep 695]: https://peps.python.org/pep-0695/ From b2f5a2a5a1c93e60b03dd0ce5e07107fec12914e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 15:22:52 -0400 Subject: [PATCH 015/100] Typevars _can_ be fully static I guess --- .../resources/mdtest/generics/pep695.md | 177 ++++++++++++++--- crates/red_knot_python_semantic/src/types.rs | 183 ++++++++++++++---- 2 files changed, 298 insertions(+), 62 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 00d3adc6e93a9..e7501ae93099b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -50,12 +50,13 @@ class C[T]: ## Subtyping and assignability -An unbounded, unconstrained typevar is assignable to itself, but is not a subtype of itself (or any -other type), since it might be specialized to `Any`, which does not participate in the subtyping -relationship. +(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static +typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual +typevars.) -It is neither assignable to or a subtype of any other type (including other typevars), since we can -make no assumption about what type it will be specialized to. +An unbounded, unconstrained, fully static typevar is a subtype of itself. It is not a subtype of any +other type (including other typevars), since we can make no assumption about what type it will be +specialized to. ```py from knot_extensions import is_assignable_to, is_subtype_of, static_assert @@ -66,23 +67,32 @@ def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) - static_assert(not is_subtype_of(T, T)) - static_assert(not is_subtype_of(U, U)) + static_assert(is_subtype_of(T, T)) + static_assert(is_subtype_of(U, U)) static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) ``` -A bounded typevar is assignable to its bound, but the bound is not assignable to the typevar, since -the typevar might be specialized to a smaller type. (This is true even if the bound is a final -class, since the typevar can still be specialized to `Never`.) +A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of +its bound. A fully static bound is not assignable to, nor a subtype of, the typevar, since the +typevar might be specialized to a smaller type. (This is true even if the bound is a final class, +since the typevar can still be specialized to `Never`.) ```py +from typing import Any from typing_extensions import final def bounded[T: int](t: list[T]) -> None: static_assert(is_assignable_to(T, int)) static_assert(not is_assignable_to(int, T)) + static_assert(is_subtype_of(T, int)) + static_assert(not is_subtype_of(int, T)) + +def bounded_by_gradual[T: Any](t: list[T]) -> None: + static_assert(is_assignable_to(T, Any)) + static_assert(is_assignable_to(Any, T)) + static_assert(not is_subtype_of(T, int)) static_assert(not is_subtype_of(int, T)) @@ -93,14 +103,14 @@ def bounded_final[T: FinalClass](t: list[T]) -> None: static_assert(is_assignable_to(T, FinalClass)) static_assert(not is_assignable_to(FinalClass, T)) - static_assert(not is_subtype_of(T, FinalClass)) + static_assert(is_subtype_of(T, FinalClass)) static_assert(not is_subtype_of(FinalClass, T)) ``` -Two distinct typevars are not assignable to each other, even if they have the same bounds, since -there is (still) no guarantee that they will be specialized to the same type. This is true even if -both typevars are bounded by the same final class, since you can specialize the typevars to `Never` -in addition to that final class. +Two distinct fully static typevars are not subtypes of each other, even if they have the same +bounds, since there is (still) no guarantee that they will be specialized to the same type. This is +true even if both typevars are bounded by the same final class, since you can specialize the +typevars to `Never` in addition to that final class. ```py def two_bounded[T: int, U: int](t: list[T], u: list[U]) -> None: @@ -118,8 +128,8 @@ def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> N static_assert(not is_subtype_of(U, T)) ``` -A constrained typevar is assignable to the union of its constraints, but not to any of the -constraints individually. None of the constraints are assignable to the typevar. +A constrained fully static typevar is assignable to the union of its constraints, but not to any of +the constraints individually. None of the constraints are subtypes of the typevar. ```py def constrained[T: (int, str)](t: list[T]) -> None: @@ -132,15 +142,31 @@ def constrained[T: (int, str)](t: list[T]) -> None: static_assert(not is_subtype_of(T, int)) static_assert(not is_subtype_of(T, str)) - static_assert(not is_subtype_of(T, int | str)) + static_assert(is_subtype_of(T, int | str)) static_assert(not is_subtype_of(int, T)) static_assert(not is_subtype_of(str, T)) static_assert(not is_subtype_of(int | str, T)) + +def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: + static_assert(is_assignable_to(T, int)) + static_assert(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, int | Any)) + static_assert(is_assignable_to(int, T)) + static_assert(is_assignable_to(Any, T)) + static_assert(is_assignable_to(int | Any, T)) + + static_assert(not is_subtype_of(T, int)) + static_assert(not is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, int | Any)) + static_assert(not is_subtype_of(int, T)) + static_assert(not is_subtype_of(Any, T)) + static_assert(not is_subtype_of(int | Any, T)) ``` -Two distinct typevars are not assignable to each other, even if they have the same constraints, and -even if any of the constraints are final. There must always be at least two distinct constraints, -meaning that there is (still) no guarantee that they will be specialized to the same type. +Two distinct fully static typevars are not subtypes of each other, even if they have the same +constraints, and even if any of the constraints are final. There must always be at least two +distinct constraints, meaning that there is (still) no guarantee that they will be specialized to +the same type. ```py def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None: @@ -164,7 +190,7 @@ def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, An ## Singletons and single-valued types (Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the -claims apply to _single-valued_ types as well.) +claims also apply to _single-valued_ types.) An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a non-singleton type. @@ -202,6 +228,113 @@ def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None static_assert(is_single_valued(T)) ``` +## Unions involving typevars + +The union of an unbounded unconstrained typevar with any other type cannot be simplified, since +there is no guarantee what type the typevar will be specialized to. + +```py +from typing import Any + +def unbounded_unconstrained[T](t: T) -> None: + def _(x: T | int) -> None: + reveal_type(x) # revealed: T | int + def _(x: T | bool) -> None: + reveal_type(x) # revealed: T | bool + def _(x: T | None) -> None: + reveal_type(x) # revealed: T | None + def _(x: T | Any) -> None: + reveal_type(x) # revealed: T | Any +``` + +The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be +specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound +cannot be simplified. (The typevar might be specialized to a different subtype of the bound.) + +```py +def bounded[T: int](t: T) -> None: + def _(x: T | int) -> None: + reveal_type(x) # revealed: int + def _(x: T | bool) -> None: + reveal_type(x) # revealed: T | bool + def _(x: T | None) -> None: + reveal_type(x) # revealed: T | None + def _(x: T | Any) -> None: + reveal_type(x) # revealed: T | Any +``` + +The union of a constrained typevar with a type depends on how that type relates to the constraints. +If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely, +if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the +union cannot be simplified. + +```py +def constrained[T: (int, bool)](t: T) -> None: + def _(x: T | int) -> None: + reveal_type(x) # revealed: int + def _(x: T | bool) -> None: + reveal_type(x) # revealed: T + def _(x: T | None) -> None: + reveal_type(x) # revealed: T | None + def _(x: T | Any) -> None: + reveal_type(x) # revealed: T | Any +``` + +## Intersections involving typevars + +The intersection of an unbounded unconstrained typevar with any other type cannot be simplified, +since there is no guarantee what type the typevar will be specialized to. + +```py +from knot_extensions import Intersection +from typing import Any + +def unbounded_unconstrained[T](t: T) -> None: + def _(x: Intersection[T, int]) -> None: + reveal_type(x) # revealed: T & int + def _(x: Intersection[T, bool]) -> None: + reveal_type(x) # revealed: T & bool + def _(x: Intersection[T, None]) -> None: + reveal_type(x) # revealed: T & None + def _(x: Intersection[T, Any]) -> None: + reveal_type(x) # revealed: T & Any +``` + +The intersection of a bounded typevar with its bound is the typevar itself. (The typevar might be +specialized to a subtype of the bound.) The intersection of a bounded typevar with a subtype of its +bound cannot be simplified. (The typevar might be specialized to a different subtype of the bound.) +The intersection of a bounded typevar with a type that is disjoint from its bound is `Never`. + +```py +def bounded[T: int](t: T) -> None: + def _(x: Intersection[T, int]) -> None: + reveal_type(x) # revealed: T + def _(x: Intersection[T, bool]) -> None: + reveal_type(x) # revealed: T & bool + def _(x: Intersection[T, None]) -> None: + reveal_type(x) # revealed: Never + def _(x: Intersection[T, Any]) -> None: + reveal_type(x) # revealed: T & Any +``` + +The intersection of a constrained typevar with a type depends on how that type relates to the +constraints. If all of the constraints are a subtype of that type, the intersection simplifies to +the typevar. Inversely, if the type is a subtype of every constraint, the intersection simplifies to +that type. If the type is disjoint from all of the constraints, the intersection simplifies to +`Never`. Otherwise, the intersection cannot be simplified. + +```py +def constrained[T: (int, bool)](t: T) -> None: + def _(x: Intersection[T, int]) -> None: + reveal_type(x) # revealed: T + def _(x: Intersection[T, bool]) -> None: + reveal_type(x) # revealed: bool + def _(x: Intersection[T, None]) -> None: + reveal_type(x) # revealed: Never + def _(x: Intersection[T, Any]) -> None: + reveal_type(x) # revealed: T & Any +``` + ## Narrowing We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar: diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 94984a46aaf77..56a0208764781 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -552,9 +552,29 @@ impl<'db> Type<'db> { return false; } + // A fully static typevar is a subtype of its upper bound, and to something similar to the + // union of its constraints. An unbound, unconstrained, fully static typevar is a subtype + // of `object` (which is handled below). + // TODO: It's not _really_ the union of its constraints because each occurrence of the + // typevar must be bound to the same type. + if let Type::TypeVar(typevar) = self { + match typevar.bound_or_constraints(db) { + None => {} + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + return bound.is_subtype_of(db, target); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + return constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_subtype_of(db, target)); + } + } + } + match (self, target) { // We should have handled these immediately above. - (Type::Dynamic(_) | Type::TypeVar(_), _) | (_, Type::Dynamic(_) | Type::TypeVar(_)) => { + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { unreachable!("Non-fully-static types do not participate in subtyping!") } @@ -567,6 +587,48 @@ impl<'db> Type<'db> { // Everything is a subtype of `object`. (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, + // A fully static typevar is always a subtype of itself, and is never a subtype of any + // other typevar, since there is no guarantee that they will be specialized to the same + // type. (This is true even if both typevars are bounded by the same final class, since + // you can specialize the typevars to `Never` in addition to that final class.) + (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { + self_typevar == other_typevar + } + + // A fully static typevar is a subtype of its upper bound, and to something similar to + // the union of its constraints. An unbound, unconstrained typevar has an implicit + // upper bound of `object`. + // TODO: It's not _really_ the union of its constraints because each occurrence of the + // typevar must be bound to the same type. + (Type::TypeVar(typevar), _) => match typevar.bound_or_constraints(db) { + None => KnownClass::Object.to_instance(db).is_subtype_of(db, target), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.is_subtype_of(db, target) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_subtype_of(db, target)), + }, + + (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { + // No types are a subtype of a bounded typevar, or of an unbounded unconstrained + // typevar, since there's no guarantee what type the typevar will be specialized + // to. If the typevar is bounded, it might be specialized to a smaller type than + // the bound. (This is true even if the bound is a final class, since the typevar + // can still be specialized to `Never`.) + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + // If the typevar is constrained, there must be multiple constraints, and the + // typevar might be specialized to any one of them. However, the constraints do not + // have to be disjoint, which means an lhs type might be a subtype of all of the + // constraints. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| self.is_subtype_of(db, *constraint)), + }, + (Type::Union(union), _) => union .elements(db) .iter() @@ -759,25 +821,6 @@ impl<'db> Type<'db> { return true; } - // A typevar is assignable to its upper bound, and to something similar to the union of - // its constraints. An unbound, unconstrained typevar is assignable to any type. - // TODO: It's not _really_ the union of its constraints because each occurrence of the - // typevar must be bound to the same type. - if let Type::TypeVar(typevar) = self { - match typevar.bound_or_constraints(db) { - None => {} - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - return bound.is_assignable_to(db, target); - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - return constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_assignable_to(db, target)); - } - } - } - match (self, target) { // Never can be assigned to any type. (Type::Never, _) => true, @@ -790,6 +833,50 @@ impl<'db> Type<'db> { // TODO this special case might be removable once the below cases are comprehensive (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, + // A typevar is always assignable to itself, and is never assignable to any other + // typevar, since there is no guarantee that they will be specialized to the same + // type. (This is true even if both typevars are bounded by the same final class, since + // you can specialize the typevars to `Never` in addition to that final class.) + (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { + self_typevar == other_typevar + } + + // A typevar is assignable to its upper bound, and to something similar to the union of + // its constraints. An unbound, unconstrained typevar has an implicit upper bound of + // `object`. + // TODO: It's not _really_ the union of its constraints because each occurrence of the + // typevar must be bound to the same type. + (Type::TypeVar(typevar), _) => match typevar.bound_or_constraints(db) { + None => KnownClass::Object + .to_instance(db) + .is_assignable_to(db, target), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.is_assignable_to(db, target) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_assignable_to(db, target)), + }, + + (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { + // No types are assignable to a bounded typevar, or to an unbounded unconstrained + // typevar, since there's no guarantee what type the typevar will be specialized + // to. If the typevar is bounded, it might be specialized to a smaller type than + // the bound. (This is true even if the bound is a final class, since the typevar + // can still be specialized to `Never`.) + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + // If the typevar is constrained, there must be multiple constraints, and the + // typevar might be specialized to any one of them. However, the constraints do not + // have to be disjoint, which means an lhs type might be assignable to all of the + // constraints. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| self.is_assignable_to(db, *constraint)), + }, + // A union is assignable to a type T iff every element of the union is assignable to T. (Type::Union(union), ty) => union .elements(db) @@ -828,22 +915,6 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), - // A typevar is always assignable to itself, and is never assignable to any other - // typevar, since there is no guarantee that they will be specialized to the same - // type. (This is true even if both typevars are bounded by the same final class, since - // you can specialize the typevars to `Never` in addition to that final class.) - (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { - self_typevar == other_typevar - } - - // No types are assignable to a typevar, since there's no guarantee what type the - // typevar will be specialized to. If the typevar is bounded, it might be specialized - // to a smaller type than the bound. (This is true even if the bound is a final class, - // since the typevar can still be specialized to `Never`.) If it is constrained, there - // must be multiple constraints, and the typevar might be specialized to any one of - // them. - (_, Type::TypeVar(_)) => false, - // A tuple type S is assignable to a tuple type T if their lengths are the same, and // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { @@ -1048,9 +1119,32 @@ impl<'db> Type<'db> { (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false, - // A typevar is never disjoint from any other type, since it might be specialized to - // `Any`. - (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => false, + // A typevar is never disjoint from itself, since all occurrences of the typevar must + // be specialized to the same type. (This is an important difference between typevars + // and `Any`!) Different typevars might be disjoint, depending on their bounds and + // constraints, which are handled below. + (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) + if self_typevar == other_typevar => + { + false + } + + // An unbounded typevar is never disjoint from any other type, since it might be + // specialized to any type. A bounded typevar is not disjoint from its bound, and is + // only disjoint from other types if its bound is. A constrained typevar is disjoint + // from a type if all of its constraints are. + (Type::TypeVar(typevar), other) | (other, Type::TypeVar(typevar)) => { + match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.is_disjoint_from(db, other) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_disjoint_from(db, other)), + } + } (Type::Union(union), other) | (other, Type::Union(union)) => union .elements(db) @@ -1360,7 +1454,6 @@ impl<'db> Type<'db> { match self { Type::Dynamic(_) => false, // A typevar is not fully static, since it can be specialized to a generic type - Type::TypeVar(_) => false, Type::Never | Type::FunctionLiteral(..) | Type::Callable( @@ -1378,6 +1471,16 @@ impl<'db> Type<'db> { | Type::KnownInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => true, + + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => true, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_fully_static(db)), + }, + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(), Type::ClassLiteral(_) | Type::Instance(_) => { // TODO: Ideally, we would iterate over the MRO of the class, check if all From 590680cd17cb87c202ee6ecf8820e3e581dbc31f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 15:39:43 -0400 Subject: [PATCH 016/100] Simplify intersections with constrained typevars --- .../resources/mdtest/generics/pep695.md | 49 +++++++++++++------ .../src/types/builder.rs | 34 +++++++++---- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index e7501ae93099b..66f0f468a5382 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -317,22 +317,39 @@ def bounded[T: int](t: T) -> None: reveal_type(x) # revealed: T & Any ``` -The intersection of a constrained typevar with a type depends on how that type relates to the -constraints. If all of the constraints are a subtype of that type, the intersection simplifies to -the typevar. Inversely, if the type is a subtype of every constraint, the intersection simplifies to -that type. If the type is disjoint from all of the constraints, the intersection simplifies to -`Never`. Otherwise, the intersection cannot be simplified. +When intersecting a constrained typevar with a type, we treat the typevar the same as a union of its +constraints. (TODO: This should be a union of "instances of _only_ this class", not "instances of +this class or a subclass".) ```py -def constrained[T: (int, bool)](t: T) -> None: +def constrained[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, int]) -> None: - reveal_type(x) # revealed: T + reveal_type(x) # revealed: int + def _(x: Intersection[T, str]) -> None: + reveal_type(x) # revealed: str def _(x: Intersection[T, bool]) -> None: reveal_type(x) # revealed: bool def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never def _(x: Intersection[T, Any]) -> None: - reveal_type(x) # revealed: T & Any + reveal_type(x) # revealed: int & Any | str & Any | bool & Any +``` + +We do the same when removing a type from a constrained typevar, since this is modeled internally as +an intersection with a negation. + +```py +from knot_extensions import Not + +def remove_constraint[T: (int, str, bool)](t: T) -> None: + def _(x: Intersection[T, Not[int]]) -> None: + reveal_type(x) # revealed: str & ~int + def _(x: Intersection[T, Not[bool]]) -> None: + reveal_type(x) # revealed: int & ~bool | str + def _(x: Intersection[T, Not[None]]) -> None: + reveal_type(x) # revealed: int | str + def _(x: Intersection[T, Not[Any]]) -> None: + reveal_type(x) # revealed: int & Any | str & Any | bool & Any ``` ## Narrowing @@ -345,18 +362,18 @@ class Q: ... def f[T: (P, Q)](t: T) -> None: if isinstance(t, P): - # TODO: revealed: Q - reveal_type(t) # revealed: T & P + reveal_type(t) # revealed: P + p: P = t else: - # TODO: revealed: P - reveal_type(t) # revealed: T & ~P + reveal_type(t) # revealed: Q & ~P + q: Q = t if isinstance(t, Q): - # TODO: revealed: P - reveal_type(t) # revealed: T & Q + reveal_type(t) # revealed: Q + q: Q = t else: - # TODO: revealed: Q - reveal_type(t) # revealed: T & ~Q + reveal_type(t) # revealed: P & ~Q + p: P = t ``` [pep 695]: https://peps.python.org/pep-0695/ diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 23b9f83807395..d28ed25bc9474 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -26,7 +26,7 @@ //! eliminate the supertype from the intersection). //! * An intersection containing two non-overlapping types should simplify to [`Type::Never`]. -use crate::types::{IntersectionType, KnownClass, Type, UnionType}; +use crate::types::{IntersectionType, KnownClass, Type, TypeVarBoundOrConstraints, UnionType}; use crate::{Db, FxOrderSet}; use smallvec::SmallVec; @@ -169,22 +169,38 @@ impl<'db> IntersectionBuilder<'db> { // (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)` // and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 & // T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea. - union + return union .elements(self.db) .iter() .map(|elem| self.clone().add_positive(*elem)) .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { builder.intersections.extend(sub.intersections); builder - }) - } else { - // If we are already a union-of-intersections, distribute the new intersected element - // across all of those intersections. - for inner in &mut self.intersections { - inner.add_positive(self.db, ty); + }); + } + + if let Type::TypeVar(typevar) = ty { + if let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + typevar.bound_or_constraints(self.db) + { + // Ditto for the union of constraints in a constrained typevar. + return constraints + .elements(self.db) + .iter() + .map(|constraint| self.clone().add_positive(*constraint)) + .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { + builder.intersections.extend(sub.intersections); + builder + }); } - self } + + // If we are already a union-of-intersections, distribute the new intersected element + // across all of those intersections. + for inner in &mut self.intersections { + inner.add_positive(self.db, ty); + } + self } pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self { From debd60a5404961cbd579bd29f5d0a192b40d6f47 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 16:10:45 -0400 Subject: [PATCH 017/100] Fix tests --- crates/red_knot_python_semantic/src/types.rs | 143 +++++++++---------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4cf02e4ad6426..21d50635c6029 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -389,6 +389,15 @@ impl<'db> Type<'db> { ClassBase::Class(_) => false, }, + Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.contains_todo(db), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .any(|constraint| constraint.contains_todo(db)), + }, + Self::Tuple(tuple) => tuple.elements(db).iter().any(|ty| ty.contains_todo(db)), Self::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)), @@ -650,8 +659,8 @@ impl<'db> Type<'db> { } // A fully static typevar is a subtype of its upper bound, and to something similar to the - // union of its constraints. An unbound, unconstrained, fully static typevar is a subtype - // of `object` (which is handled below). + // union of its constraints. An unbound, unconstrained, fully static typevar has an + // implicit upper bound of `object` (which is handled below). // TODO: It's not _really_ the union of its constraints because each occurrence of the // typevar must be bound to the same type. if let Type::TypeVar(typevar) = self { @@ -692,40 +701,6 @@ impl<'db> Type<'db> { self_typevar == other_typevar } - // A fully static typevar is a subtype of its upper bound, and to something similar to - // the union of its constraints. An unbound, unconstrained typevar has an implicit - // upper bound of `object`. - // TODO: It's not _really_ the union of its constraints because each occurrence of the - // typevar must be bound to the same type. - (Type::TypeVar(typevar), _) => match typevar.bound_or_constraints(db) { - None => KnownClass::Object.to_instance(db).is_subtype_of(db, target), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_subtype_of(db, target) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_subtype_of(db, target)), - }, - - (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { - // No types are a subtype of a bounded typevar, or of an unbounded unconstrained - // typevar, since there's no guarantee what type the typevar will be specialized - // to. If the typevar is bounded, it might be specialized to a smaller type than - // the bound. (This is true even if the bound is a final class, since the typevar - // can still be specialized to `Never`.) - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - // If the typevar is constrained, there must be multiple constraints, and the - // typevar might be specialized to any one of them. However, the constraints do not - // have to be disjoint, which means an lhs type might be a subtype of all of the - // constraints. - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| self.is_subtype_of(db, *constraint)), - }, - (Type::Union(union), _) => union .elements(db) .iter() @@ -755,6 +730,28 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_subtype_of(db, target)), + // Other than the special cases enumerated above, typevars are never subtypes of any + // other variants. + (Type::TypeVar(_), _) => false, + + (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { + // No types are a subtype of a bounded typevar, or of an unbounded unconstrained + // typevar, since there's no guarantee what type the typevar will be specialized + // to. If the typevar is bounded, it might be specialized to a smaller type than + // the bound. (This is true even if the bound is a final class, since the typevar + // can still be specialized to `Never`.) + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + // If the typevar is constrained, there must be multiple constraints, and the + // typevar might be specialized to any one of them. However, the constraints do not + // have to be disjoint, which means an lhs type might be a subtype of all of the + // constraints. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| self.is_subtype_of(db, *constraint)), + }, + // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. (left, Type::AlwaysFalsy) => left.bool(db).is_always_false(), @@ -913,6 +910,26 @@ impl<'db> Type<'db> { return true; } + // A typevar is assignable to its upper bound, and to something similar to the union of + // its constraints. An unbound, unconstrained typevar has an implicit upper bound of + // `object` (which is handled below). + // TODO: It's not _really_ the union of its constraints because each occurrence of the + // typevar must be bound to the same type. + if let Type::TypeVar(typevar) = self { + match typevar.bound_or_constraints(db) { + None => {} + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + return bound.is_assignable_to(db, target); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + return constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_assignable_to(db, target)); + } + } + } + match (self, target) { // Never can be assigned to any type. (Type::Never, _) => true, @@ -933,42 +950,6 @@ impl<'db> Type<'db> { self_typevar == other_typevar } - // A typevar is assignable to its upper bound, and to something similar to the union of - // its constraints. An unbound, unconstrained typevar has an implicit upper bound of - // `object`. - // TODO: It's not _really_ the union of its constraints because each occurrence of the - // typevar must be bound to the same type. - (Type::TypeVar(typevar), _) => match typevar.bound_or_constraints(db) { - None => KnownClass::Object - .to_instance(db) - .is_assignable_to(db, target), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_assignable_to(db, target) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_assignable_to(db, target)), - }, - - (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { - // No types are assignable to a bounded typevar, or to an unbounded unconstrained - // typevar, since there's no guarantee what type the typevar will be specialized - // to. If the typevar is bounded, it might be specialized to a smaller type than - // the bound. (This is true even if the bound is a final class, since the typevar - // can still be specialized to `Never`.) - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - // If the typevar is constrained, there must be multiple constraints, and the - // typevar might be specialized to any one of them. However, the constraints do not - // have to be disjoint, which means an lhs type might be assignable to all of the - // constraints. - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| self.is_assignable_to(db, *constraint)), - }, - // A union is assignable to a type T iff every element of the union is assignable to T. (Type::Union(union), ty) => union .elements(db) @@ -1007,6 +988,24 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), + (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { + // No types are assignable to a bounded typevar, or to an unbounded unconstrained + // typevar, since there's no guarantee what type the typevar will be specialized + // to. If the typevar is bounded, it might be specialized to a smaller type than + // the bound. (This is true even if the bound is a final class, since the typevar + // can still be specialized to `Never`.) + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + // If the typevar is constrained, there must be multiple constraints, and the + // typevar might be specialized to any one of them. However, the constraints do not + // have to be disjoint, which means an lhs type might be assignable to all of the + // constraints. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| self.is_assignable_to(db, *constraint)), + }, + // A tuple type S is assignable to a tuple type T if their lengths are the same, and // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { From aa391fd2d4a478046dd5a9ed39bd67a2cebfba63 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 16:11:32 -0400 Subject: [PATCH 018/100] lint --- .../resources/mdtest/generics/pep695.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 66f0f468a5382..6689b60c2c6a5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -239,10 +239,13 @@ from typing import Any def unbounded_unconstrained[T](t: T) -> None: def _(x: T | int) -> None: reveal_type(x) # revealed: T | int + def _(x: T | bool) -> None: reveal_type(x) # revealed: T | bool + def _(x: T | None) -> None: reveal_type(x) # revealed: T | None + def _(x: T | Any) -> None: reveal_type(x) # revealed: T | Any ``` @@ -255,10 +258,13 @@ cannot be simplified. (The typevar might be specialized to a different subtype o def bounded[T: int](t: T) -> None: def _(x: T | int) -> None: reveal_type(x) # revealed: int + def _(x: T | bool) -> None: reveal_type(x) # revealed: T | bool + def _(x: T | None) -> None: reveal_type(x) # revealed: T | None + def _(x: T | Any) -> None: reveal_type(x) # revealed: T | Any ``` @@ -272,10 +278,13 @@ union cannot be simplified. def constrained[T: (int, bool)](t: T) -> None: def _(x: T | int) -> None: reveal_type(x) # revealed: int + def _(x: T | bool) -> None: reveal_type(x) # revealed: T + def _(x: T | None) -> None: reveal_type(x) # revealed: T | None + def _(x: T | Any) -> None: reveal_type(x) # revealed: T | Any ``` @@ -292,10 +301,13 @@ from typing import Any def unbounded_unconstrained[T](t: T) -> None: def _(x: Intersection[T, int]) -> None: reveal_type(x) # revealed: T & int + def _(x: Intersection[T, bool]) -> None: reveal_type(x) # revealed: T & bool + def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: T & None + def _(x: Intersection[T, Any]) -> None: reveal_type(x) # revealed: T & Any ``` @@ -309,10 +321,13 @@ The intersection of a bounded typevar with a type that is disjoint from its boun def bounded[T: int](t: T) -> None: def _(x: Intersection[T, int]) -> None: reveal_type(x) # revealed: T + def _(x: Intersection[T, bool]) -> None: reveal_type(x) # revealed: T & bool + def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never + def _(x: Intersection[T, Any]) -> None: reveal_type(x) # revealed: T & Any ``` @@ -325,12 +340,16 @@ this class or a subclass".) def constrained[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, int]) -> None: reveal_type(x) # revealed: int + def _(x: Intersection[T, str]) -> None: reveal_type(x) # revealed: str + def _(x: Intersection[T, bool]) -> None: reveal_type(x) # revealed: bool + def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never + def _(x: Intersection[T, Any]) -> None: reveal_type(x) # revealed: int & Any | str & Any | bool & Any ``` @@ -344,10 +363,13 @@ from knot_extensions import Not def remove_constraint[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, Not[int]]) -> None: reveal_type(x) # revealed: str & ~int + def _(x: Intersection[T, Not[bool]]) -> None: reveal_type(x) # revealed: int & ~bool | str + def _(x: Intersection[T, Not[None]]) -> None: reveal_type(x) # revealed: int | str + def _(x: Intersection[T, Not[Any]]) -> None: reveal_type(x) # revealed: int & Any | str & Any | bool & Any ``` From e57e62e01353fb3e7c5654de3a2159571c1368c4 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 1 Apr 2025 17:07:55 -0400 Subject: [PATCH 019/100] Update crates/red_knot_python_semantic/src/types/type_ordering.rs Co-authored-by: Alex Waygood --- crates/red_knot_python_semantic/src/types/type_ordering.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 67f00941b4057..1b017f3117f6a 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -118,6 +118,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::Instance(_), _) => Ordering::Less, (_, Type::Instance(_)) => Ordering::Greater, + (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), (Type::TypeVar(_), _) => Ordering::Less, (_, Type::TypeVar(_)) => Ordering::Greater, From 3df79cc1a40c209dc731974983e52f2d6e41c355 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 09:15:23 -0400 Subject: [PATCH 020/100] Clarify that typevar is subtype of object too --- .../resources/mdtest/generics/pep695.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 6689b60c2c6a5..31da344542478 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -54,21 +54,26 @@ class C[T]: typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual typevars.) -An unbounded, unconstrained, fully static typevar is a subtype of itself. It is not a subtype of any -other type (including other typevars), since we can make no assumption about what type it will be -specialized to. +We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be +specialized to. Properties are true of the typevar only if they are true for every valid +specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type +(including other typevars). ```py from knot_extensions import is_assignable_to, is_subtype_of, static_assert def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: static_assert(is_assignable_to(T, T)) + static_assert(is_assignable_to(T, object)) static_assert(is_assignable_to(U, U)) + static_assert(is_assignable_to(U, object)) static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) static_assert(is_subtype_of(T, T)) + static_assert(is_subtype_of(T, object)) static_assert(is_subtype_of(U, U)) + static_assert(is_subtype_of(U, object)) static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) ``` From 5b08e93bee98a4731ca982ffac61729e23783c66 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 09:23:27 -0400 Subject: [PATCH 021/100] Clarify non-fully-static bounded typevars aren't subtypes --- .../resources/mdtest/generics/pep695.md | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 31da344542478..8d2ef2e6bdc16 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -48,6 +48,33 @@ class C[T]: reveal_type(x) # revealed: T ``` +## Fully static typevars + +We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This +is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This +is similar to how you can assign an expression whose type is not fully static to a target whose type +is.) + +```py +from knot_extensions import is_fully_static, static_assert +from typing import Any + +def unbounded_unconstrained[T](t: list[T]) -> None: + static_assert(is_fully_static(T)) + +def bounded[T: int](t: list[T]) -> None: + static_assert(is_fully_static(T)) + +def bounded_by_gradual[T: Any](t: list[T]) -> None: + static_assert(not is_fully_static(T)) + +def constrained[T: (int, str)](t: list[T]) -> None: + static_assert(is_fully_static(T)) + +def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: + static_assert(not is_fully_static(T)) +``` + ## Subtyping and assignability (Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static @@ -79,9 +106,10 @@ def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: ``` A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of -its bound. A fully static bound is not assignable to, nor a subtype of, the typevar, since the -typevar might be specialized to a smaller type. (This is true even if the bound is a final class, -since the typevar can still be specialized to `Never`.) +its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does +not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the +typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound +is a final class, since the typevar can still be specialized to `Never`.) ```py from typing import Any From 82e810f96805f5f34ab629ffe6c4ff42321c0a19 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 10:06:26 -0400 Subject: [PATCH 022/100] Add more tests for constrained gradual typevars --- .../resources/mdtest/generics/pep695.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 8d2ef2e6bdc16..9680b03824142 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -125,7 +125,11 @@ def bounded[T: int](t: list[T]) -> None: def bounded_by_gradual[T: Any](t: list[T]) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) + static_assert(is_assignable_to(T, int)) + static_assert(not is_assignable_to(int, T)) + static_assert(not is_subtype_of(T, Any)) + static_assert(not is_subtype_of(Any, T)) static_assert(not is_subtype_of(T, int)) static_assert(not is_subtype_of(int, T)) @@ -182,18 +186,26 @@ def constrained[T: (int, str)](t: list[T]) -> None: def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: static_assert(is_assignable_to(T, int)) + static_assert(not is_assignable_to(T, str)) static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(T, int | Any)) + static_assert(is_assignable_to(T, int | str)) static_assert(is_assignable_to(int, T)) + static_assert(not is_assignable_to(str, T)) static_assert(is_assignable_to(Any, T)) static_assert(is_assignable_to(int | Any, T)) + static_assert(not is_assignable_to(int | str, T)) static_assert(not is_subtype_of(T, int)) + static_assert(not is_subtype_of(T, str)) static_assert(not is_subtype_of(T, Any)) static_assert(not is_subtype_of(T, int | Any)) + static_assert(not is_subtype_of(T, int | str)) static_assert(not is_subtype_of(int, T)) + static_assert(not is_subtype_of(str, T)) static_assert(not is_subtype_of(Any, T)) static_assert(not is_subtype_of(int | Any, T)) + static_assert(not is_subtype_of(int | str, T)) ``` Two distinct fully static typevars are not subtypes of each other, even if they have the same From a3d725387c8432903980b74333f141610f3c684d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 10:10:59 -0400 Subject: [PATCH 023/100] Update crates/red_knot_python_semantic/src/types.rs Co-authored-by: Carl Meyer --- crates/red_knot_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 21d50635c6029..73b0a51e7d247 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -304,7 +304,7 @@ pub enum Type<'db> { /// An instance of a typevar in a generic class or function. When the generic class or function /// is specialized, we will replace this typevar with its specialization. TypeVar(TypeVarInstance<'db>), - // TODO protocols, callable types, overloads, generics + // TODO protocols, overloads, generics } #[salsa::tracked] From 15682d582c9a955395627f51d471c9bbeb43f980 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 14:52:38 -0400 Subject: [PATCH 024/100] Simplify intersections with constrained typevars w/o glossing into union --- .../resources/mdtest/generics/pep695.md | 33 ++++-- .../src/types/builder.rs | 109 ++++++++++++++---- 2 files changed, 106 insertions(+), 36 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 9680b03824142..11b863adffec2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -384,19 +384,22 @@ this class or a subclass".) ```py def constrained[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, int]) -> None: - reveal_type(x) # revealed: int + # With OneOf this would be OneOf[int, bool] + reveal_type(x) # revealed: T & int def _(x: Intersection[T, str]) -> None: - reveal_type(x) # revealed: str + # In theory we could simplify this to `str` since none of the other constraints are subtypes + # of `str`. But leaving this unsimplified is not incorrect. + reveal_type(x) # revealed: T & str def _(x: Intersection[T, bool]) -> None: - reveal_type(x) # revealed: bool + reveal_type(x) # revealed: T & bool def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never def _(x: Intersection[T, Any]) -> None: - reveal_type(x) # revealed: int & Any | str & Any | bool & Any + reveal_type(x) # revealed: T & Any ``` We do the same when removing a type from a constrained typevar, since this is modeled internally as @@ -407,16 +410,22 @@ from knot_extensions import Not def remove_constraint[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, Not[int]]) -> None: - reveal_type(x) # revealed: str & ~int + reveal_type(x) # revealed: str + + def _(x: Intersection[T, Not[str]]) -> None: + reveal_type(x) # revealed: T & ~str def _(x: Intersection[T, Not[bool]]) -> None: - reveal_type(x) # revealed: int & ~bool | str + reveal_type(x) # revealed: T & ~bool + + def _(x: Intersection[T, Not[int], Not[str]]) -> None: + reveal_type(x) # revealed: Never def _(x: Intersection[T, Not[None]]) -> None: - reveal_type(x) # revealed: int | str + reveal_type(x) # revealed: T def _(x: Intersection[T, Not[Any]]) -> None: - reveal_type(x) # revealed: int & Any | str & Any | bool & Any + reveal_type(x) # revealed: T & Any ``` ## Narrowing @@ -429,17 +438,17 @@ class Q: ... def f[T: (P, Q)](t: T) -> None: if isinstance(t, P): - reveal_type(t) # revealed: P + reveal_type(t) # revealed: T & P p: P = t else: - reveal_type(t) # revealed: Q & ~P + reveal_type(t) # revealed: Q q: Q = t if isinstance(t, Q): - reveal_type(t) # revealed: Q + reveal_type(t) # revealed: T & Q q: Q = t else: - reveal_type(t) # revealed: P & ~Q + reveal_type(t) # revealed: P p: P = t ``` diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index d28ed25bc9474..5dac85e4bf19c 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -169,38 +169,22 @@ impl<'db> IntersectionBuilder<'db> { // (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)` // and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 & // T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea. - return union + union .elements(self.db) .iter() .map(|elem| self.clone().add_positive(*elem)) .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { builder.intersections.extend(sub.intersections); builder - }); - } - - if let Type::TypeVar(typevar) = ty { - if let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - typevar.bound_or_constraints(self.db) - { - // Ditto for the union of constraints in a constrained typevar. - return constraints - .elements(self.db) - .iter() - .map(|constraint| self.clone().add_positive(*constraint)) - .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { - builder.intersections.extend(sub.intersections); - builder - }); + }) + } else { + // If we are already a union-of-intersections, distribute the new intersected element + // across all of those intersections. + for inner in &mut self.intersections { + inner.add_positive(self.db, ty); } + self } - - // If we are already a union-of-intersections, distribute the new intersected element - // across all of those intersections. - for inner in &mut self.intersections { - inner.add_positive(self.db, ty); - } - self } pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self { @@ -501,7 +485,84 @@ impl<'db> InnerIntersectionBuilder<'db> { } } + /// Tries to simplify any constrained typevars in the intersection. If the intersection + /// contains negative entries for all but one of the typevar's constraints, we can remove the + /// negative constraints and replace the typevar with the remaining positive constraint. If the + /// intersection contains negative entries for all of the constraints, the overall intersection + /// is `Never`. + fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) { + let mut to_add = SmallVec::<[Type<'db>; 1]>::new(); + let mut positive_to_remove = SmallVec::<[usize; 1]>::new(); + let mut negative_to_remove = Vec::new(); + + for (typevar_index, ty) in self.positive.iter().enumerate() { + let Type::TypeVar(typevar) = ty else { + continue; + }; + let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + typevar.bound_or_constraints(db) + else { + continue; + }; + + // Determine which constraints appear as negative entries in the intersection. Note + // that we shouldn't have duplicate entries in the positive or negative lists, so we + // don't need to worry about finding any particular constraint more than once. + let constraints = constraints.elements(db); + let mut to_remove = Vec::with_capacity(constraints.len()); + let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect(); + for (negative_index, negative) in self.negative.iter().enumerate() { + // This linear search should be fine as long as we don't encounter typevars with + // thousands of constraints. + let matching_constraints = constraints + .iter() + .enumerate() + .filter(|(_, c)| c.is_subtype_of(db, *negative)); + for (constraint_index, _) in matching_constraints { + to_remove.push(negative_index); + remaining_constraints[constraint_index] = None; + } + } + + let mut iter = remaining_constraints.into_iter().flatten(); + let Some(remaining_constraint) = iter.next() else { + // All of the typevar constraints have been removed, so the entire intersection is + // `Never`. + *self = Self::default(); + self.positive.insert(Type::Never); + return; + }; + + let more_than_one_remaining_constraint = iter.next().is_some(); + if more_than_one_remaining_constraint { + // This typevar cannot be simplified. + continue; + } + + // Only one typevar constraint remains. Remove all of the negative constraints, and replace + // the typevar itself with the remaining positive constraint. + to_add.push(remaining_constraint); + positive_to_remove.push(typevar_index); + negative_to_remove.extend(to_remove); + } + + for index in positive_to_remove.into_iter().rev() { + self.positive.swap_remove_index(index); + } + + negative_to_remove.sort_unstable(); + negative_to_remove.dedup(); + for index in negative_to_remove.into_iter().rev() { + self.negative.swap_remove_index(index); + } + + for remaining_constraint in to_add { + self.add_positive(db, remaining_constraint); + } + } + fn build(mut self, db: &'db dyn Db) -> Type<'db> { + self.simplify_constrained_typevars(db); match (self.positive.len(), self.negative.len()) { (0, 0) => Type::object(db), (1, 0) => self.positive[0], From 9e07efe8241a26d6cf444b0ff54fa1c50c10016f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 15:05:40 -0400 Subject: [PATCH 025/100] Simplify positive intersections too --- .../resources/mdtest/generics/pep695.md | 10 ++--- .../src/types/builder.rs | 41 +++++++++++++++---- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 11b863adffec2..4c37b9d1ba055 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -388,12 +388,10 @@ def constrained[T: (int, str, bool)](t: T) -> None: reveal_type(x) # revealed: T & int def _(x: Intersection[T, str]) -> None: - # In theory we could simplify this to `str` since none of the other constraints are subtypes - # of `str`. But leaving this unsimplified is not incorrect. - reveal_type(x) # revealed: T & str + reveal_type(x) # revealed: str def _(x: Intersection[T, bool]) -> None: - reveal_type(x) # revealed: T & bool + reveal_type(x) # revealed: bool def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never @@ -438,14 +436,14 @@ class Q: ... def f[T: (P, Q)](t: T) -> None: if isinstance(t, P): - reveal_type(t) # revealed: T & P + reveal_type(t) # revealed: P p: P = t else: reveal_type(t) # revealed: Q q: Q = t if isinstance(t, Q): - reveal_type(t) # revealed: T & Q + reveal_type(t) # revealed: Q q: Q = t else: reveal_type(t) # revealed: P diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 5dac85e4bf19c..ff03e4453aed2 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -485,11 +485,17 @@ impl<'db> InnerIntersectionBuilder<'db> { } } - /// Tries to simplify any constrained typevars in the intersection. If the intersection - /// contains negative entries for all but one of the typevar's constraints, we can remove the - /// negative constraints and replace the typevar with the remaining positive constraint. If the - /// intersection contains negative entries for all of the constraints, the overall intersection - /// is `Never`. + /// Tries to simplify any constrained typevars in the intersection: + /// + /// - If the intersection contains a positive entry for exactly one of the constraints, we can + /// remove the typevar (effectively replacing it with that one positive constraint). + /// + /// - If the intersection contains negative entries for all but one of the constraints, we can + /// remove the negative constraints and replace the typevar with the remaining positive + /// constraint. + /// + /// - If the intersection contains negative entries for all of the constraints, the overall + /// intersection is `Never`. fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) { let mut to_add = SmallVec::<[Type<'db>; 1]>::new(); let mut positive_to_remove = SmallVec::<[usize; 1]>::new(); @@ -505,10 +511,28 @@ impl<'db> InnerIntersectionBuilder<'db> { continue; }; - // Determine which constraints appear as negative entries in the intersection. Note + // Determine which constraints appear as positive entries in the intersection. Note // that we shouldn't have duplicate entries in the positive or negative lists, so we // don't need to worry about finding any particular constraint more than once. let constraints = constraints.elements(db); + let mut positive_constraint_count = 0; + for positive in &self.positive { + // This linear search should be fine as long as we don't encounter typevars with + // thousands of constraints. + positive_constraint_count += constraints + .iter() + .filter(|c| c.is_subtype_of(db, *positive)) + .count(); + } + + // If precisely one constraint appears as a positive element, we can replace the + // typevar with that positive constraint. + if positive_constraint_count == 1 { + positive_to_remove.push(typevar_index); + continue; + } + + // Determine which constraints appear as negative entries in the intersection. let mut to_remove = Vec::with_capacity(constraints.len()); let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect(); for (negative_index, negative) in self.negative.iter().enumerate() { @@ -539,13 +563,14 @@ impl<'db> InnerIntersectionBuilder<'db> { continue; } - // Only one typevar constraint remains. Remove all of the negative constraints, and replace - // the typevar itself with the remaining positive constraint. + // Only one typevar constraint remains. Remove all of the negative constraints, and + // replace the typevar itself with the remaining positive constraint. to_add.push(remaining_constraint); positive_to_remove.push(typevar_index); negative_to_remove.extend(to_remove); } + // We don't need to sort the positive list, since we only append to it in increasing order. for index in positive_to_remove.into_iter().rev() { self.positive.swap_remove_index(index); } From fb63c2241290d4a2d034b9d45a25afa68f28ee56 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 15:16:22 -0400 Subject: [PATCH 026/100] Intersection of constraints is subtype of typevar --- .../resources/mdtest/generics/pep695.md | 8 +- crates/red_knot_python_semantic/src/types.rs | 74 +++++++++---------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 4c37b9d1ba055..98e99af1f6eb5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -166,9 +166,13 @@ def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> N ``` A constrained fully static typevar is assignable to the union of its constraints, but not to any of -the constraints individually. None of the constraints are subtypes of the typevar. +the constraints individually. None of the constraints are subtypes of the typevar, though the +intersection of all of its contraints is a subtype of the typevar. (Though that intersection is +likely to be `Never` in practice.) ```py +from knot_extensions import Intersection + def constrained[T: (int, str)](t: list[T]) -> None: static_assert(not is_assignable_to(T, int)) static_assert(not is_assignable_to(T, str)) @@ -176,6 +180,7 @@ def constrained[T: (int, str)](t: list[T]) -> None: static_assert(not is_assignable_to(int, T)) static_assert(not is_assignable_to(str, T)) static_assert(not is_assignable_to(int | str, T)) + static_assert(is_assignable_to(Intersection[int, str], T)) static_assert(not is_subtype_of(T, int)) static_assert(not is_subtype_of(T, str)) @@ -183,6 +188,7 @@ def constrained[T: (int, str)](t: list[T]) -> None: static_assert(not is_subtype_of(int, T)) static_assert(not is_subtype_of(str, T)) static_assert(not is_subtype_of(int | str, T)) + static_assert(is_subtype_of(Intersection[int, str], T)) def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: static_assert(is_assignable_to(T, int)) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 21d50635c6029..0fcb0bd545b8c 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -711,25 +711,6 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| self.is_subtype_of(db, elem_ty)), - // If both sides are intersections we need to handle the right side first - // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, - // but none of A, B, or C is a subtype of (A & B). - (_, Type::Intersection(intersection)) => { - intersection - .positive(db) - .iter() - .all(|&pos_ty| self.is_subtype_of(db, pos_ty)) - && intersection - .negative(db) - .iter() - .all(|&neg_ty| self.is_disjoint_from(db, neg_ty)) - } - - (Type::Intersection(intersection), _) => intersection - .positive(db) - .iter() - .any(|&elem_ty| elem_ty.is_subtype_of(db, target)), - // Other than the special cases enumerated above, typevars are never subtypes of any // other variants. (Type::TypeVar(_), _) => false, @@ -752,6 +733,25 @@ impl<'db> Type<'db> { .all(|constraint| self.is_subtype_of(db, *constraint)), }, + // If both sides are intersections we need to handle the right side first + // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, + // but none of A, B, or C is a subtype of (A & B). + (_, Type::Intersection(intersection)) => { + intersection + .positive(db) + .iter() + .all(|&pos_ty| self.is_subtype_of(db, pos_ty)) + && intersection + .negative(db) + .iter() + .all(|&neg_ty| self.is_disjoint_from(db, neg_ty)) + } + + (Type::Intersection(intersection), _) => intersection + .positive(db) + .iter() + .any(|&elem_ty| elem_ty.is_subtype_of(db, target)), + // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. (left, Type::AlwaysFalsy) => left.bool(db).is_always_false(), @@ -962,6 +962,24 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)), + (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { + // No types are assignable to a bounded typevar, or to an unbounded unconstrained + // typevar, since there's no guarantee what type the typevar will be specialized + // to. If the typevar is bounded, it might be specialized to a smaller type than + // the bound. (This is true even if the bound is a final class, since the typevar + // can still be specialized to `Never`.) + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + // If the typevar is constrained, there must be multiple constraints, and the + // typevar might be specialized to any one of them. However, the constraints do not + // have to be disjoint, which means an lhs type might be assignable to all of the + // constraints. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| self.is_assignable_to(db, *constraint)), + }, + // If both sides are intersections we need to handle the right side first // (A & B & C) is assignable to (A & B) because the left is assignable to both A and B, // but none of A, B, or C is assignable to (A & B). @@ -988,24 +1006,6 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), - (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { - // No types are assignable to a bounded typevar, or to an unbounded unconstrained - // typevar, since there's no guarantee what type the typevar will be specialized - // to. If the typevar is bounded, it might be specialized to a smaller type than - // the bound. (This is true even if the bound is a final class, since the typevar - // can still be specialized to `Never`.) - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - // If the typevar is constrained, there must be multiple constraints, and the - // typevar might be specialized to any one of them. However, the constraints do not - // have to be disjoint, which means an lhs type might be assignable to all of the - // constraints. - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| self.is_assignable_to(db, *constraint)), - }, - // A tuple type S is assignable to a tuple type T if their lengths are the same, and // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { From 345905629d574a796ec06a553a40372fa7d7fe3a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 19:37:07 -0400 Subject: [PATCH 027/100] Better descriptions of intersections of constrained typevars --- .../resources/mdtest/generics/pep695.md | 252 +++++++++++------- 1 file changed, 161 insertions(+), 91 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 98e99af1f6eb5..24126928e4101 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -89,6 +89,11 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no ```py from knot_extensions import is_assignable_to, is_subtype_of, static_assert +class Super: ... +class Base(Super): ... +class Sub(Base): ... +class Unrelated: ... + def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: static_assert(is_assignable_to(T, T)) static_assert(is_assignable_to(T, object)) @@ -115,23 +120,31 @@ is a final class, since the typevar can still be specialized to `Never`.) from typing import Any from typing_extensions import final -def bounded[T: int](t: list[T]) -> None: - static_assert(is_assignable_to(T, int)) - static_assert(not is_assignable_to(int, T)) +def bounded[T: Super](t: list[T]) -> None: + static_assert(is_assignable_to(T, Super)) + static_assert(not is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Sub, T)) - static_assert(is_subtype_of(T, int)) - static_assert(not is_subtype_of(int, T)) + static_assert(is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Sub, T)) def bounded_by_gradual[T: Any](t: list[T]) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) - static_assert(is_assignable_to(T, int)) - static_assert(not is_assignable_to(int, T)) + static_assert(is_assignable_to(T, Super)) + static_assert(not is_assignable_to(Super, T)) + static_assert(is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(Sub, T)) static_assert(not is_subtype_of(T, Any)) static_assert(not is_subtype_of(Any, T)) - static_assert(not is_subtype_of(T, int)) - static_assert(not is_subtype_of(int, T)) + static_assert(not is_subtype_of(T, Super)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(Sub, T)) @final class FinalClass: ... @@ -150,7 +163,7 @@ true even if both typevars are bounded by the same final class, since you can sp typevars to `Never` in addition to that final class. ```py -def two_bounded[T: int, U: int](t: list[T], u: list[U]) -> None: +def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -173,45 +186,65 @@ likely to be `Never` in practice.) ```py from knot_extensions import Intersection -def constrained[T: (int, str)](t: list[T]) -> None: - static_assert(not is_assignable_to(T, int)) - static_assert(not is_assignable_to(T, str)) - static_assert(is_assignable_to(T, int | str)) - static_assert(not is_assignable_to(int, T)) - static_assert(not is_assignable_to(str, T)) - static_assert(not is_assignable_to(int | str, T)) - static_assert(is_assignable_to(Intersection[int, str], T)) - - static_assert(not is_subtype_of(T, int)) - static_assert(not is_subtype_of(T, str)) - static_assert(is_subtype_of(T, int | str)) - static_assert(not is_subtype_of(int, T)) - static_assert(not is_subtype_of(str, T)) - static_assert(not is_subtype_of(int | str, T)) - static_assert(is_subtype_of(Intersection[int, str], T)) - -def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: - static_assert(is_assignable_to(T, int)) - static_assert(not is_assignable_to(T, str)) +def constrained[T: (Base, Unrelated)](t: list[T]) -> None: + static_assert(not is_assignable_to(T, Super)) + static_assert(not is_assignable_to(T, Base)) + static_assert(not is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Unrelated)) + static_assert(is_assignable_to(T, Super | Unrelated)) + static_assert(is_assignable_to(T, Base | Unrelated)) + static_assert(not is_assignable_to(T, Sub | Unrelated)) + static_assert(not is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Unrelated, T)) + static_assert(not is_assignable_to(Super | Unrelated, T)) + static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + + static_assert(not is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Base)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Unrelated)) + static_assert(is_subtype_of(T, Super | Unrelated)) + static_assert(is_subtype_of(T, Base | Unrelated)) + static_assert(not is_subtype_of(T, Sub | Unrelated)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Unrelated, T)) + static_assert(not is_subtype_of(Super | Unrelated, T)) + static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) + +def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None: + static_assert(is_assignable_to(T, Super)) + static_assert(is_assignable_to(T, Base)) + static_assert(not is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Unrelated)) static_assert(is_assignable_to(T, Any)) - static_assert(is_assignable_to(T, int | Any)) - static_assert(is_assignable_to(T, int | str)) - static_assert(is_assignable_to(int, T)) - static_assert(not is_assignable_to(str, T)) + static_assert(is_assignable_to(T, Super | Any)) + static_assert(is_assignable_to(T, Super | Unrelated)) + static_assert(not is_assignable_to(Super, T)) + static_assert(is_assignable_to(Base, T)) + static_assert(not is_assignable_to(Unrelated, T)) static_assert(is_assignable_to(Any, T)) - static_assert(is_assignable_to(int | Any, T)) - static_assert(not is_assignable_to(int | str, T)) - - static_assert(not is_subtype_of(T, int)) - static_assert(not is_subtype_of(T, str)) + static_assert(not is_assignable_to(Super | Any, T)) + static_assert(is_assignable_to(Base | Any, T)) + static_assert(not is_assignable_to(Super | Unrelated, T)) + static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + static_assert(is_assignable_to(Intersection[Base, Any], T)) + + static_assert(not is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Base)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Unrelated)) static_assert(not is_subtype_of(T, Any)) - static_assert(not is_subtype_of(T, int | Any)) - static_assert(not is_subtype_of(T, int | str)) - static_assert(not is_subtype_of(int, T)) - static_assert(not is_subtype_of(str, T)) + static_assert(not is_subtype_of(T, Super | Any)) + static_assert(not is_subtype_of(T, Super | Unrelated)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Base, T)) + static_assert(not is_subtype_of(Unrelated, T)) static_assert(not is_subtype_of(Any, T)) - static_assert(not is_subtype_of(int | Any, T)) - static_assert(not is_subtype_of(int | str, T)) + static_assert(not is_subtype_of(Super | Any, T)) + static_assert(not is_subtype_of(Base | Any, T)) + static_assert(not is_subtype_of(Super | Unrelated, T)) + static_assert(not is_subtype_of(Intersection[Base, Unrelated], T)) + static_assert(not is_subtype_of(Intersection[Base, Any], T)) ``` Two distinct fully static typevars are not subtypes of each other, even if they have the same @@ -287,15 +320,23 @@ there is no guarantee what type the typevar will be specialized to. ```py from typing import Any +class Super: ... +class Base(Super): ... +class Sub(Base): ... +class Unrelated: ... + def unbounded_unconstrained[T](t: T) -> None: - def _(x: T | int) -> None: - reveal_type(x) # revealed: T | int + def _(x: T | Super) -> None: + reveal_type(x) # revealed: T | Super + + def _(x: T | Base) -> None: + reveal_type(x) # revealed: T | Base - def _(x: T | bool) -> None: - reveal_type(x) # revealed: T | bool + def _(x: T | Sub) -> None: + reveal_type(x) # revealed: T | Sub - def _(x: T | None) -> None: - reveal_type(x) # revealed: T | None + def _(x: T | Unrelated) -> None: + reveal_type(x) # revealed: T | Unrelated def _(x: T | Any) -> None: reveal_type(x) # revealed: T | Any @@ -306,15 +347,18 @@ specialized to a subtype of the bound.) The union of a bounded typevar with a su cannot be simplified. (The typevar might be specialized to a different subtype of the bound.) ```py -def bounded[T: int](t: T) -> None: - def _(x: T | int) -> None: - reveal_type(x) # revealed: int +def bounded[T: Base](t: T) -> None: + def _(x: T | Super) -> None: + reveal_type(x) # revealed: Super - def _(x: T | bool) -> None: - reveal_type(x) # revealed: T | bool + def _(x: T | Base) -> None: + reveal_type(x) # revealed: Base - def _(x: T | None) -> None: - reveal_type(x) # revealed: T | None + def _(x: T | Sub) -> None: + reveal_type(x) # revealed: T | Sub + + def _(x: T | Unrelated) -> None: + reveal_type(x) # revealed: T | Unrelated def _(x: T | Any) -> None: reveal_type(x) # revealed: T | Any @@ -326,15 +370,18 @@ if the type is a subtype of every constraint, the union simplifies to the typeva union cannot be simplified. ```py -def constrained[T: (int, bool)](t: T) -> None: - def _(x: T | int) -> None: - reveal_type(x) # revealed: int +def constrained[T: (Base, Sub)](t: T) -> None: + def _(x: T | Super) -> None: + reveal_type(x) # revealed: Super + + def _(x: T | Base) -> None: + reveal_type(x) # revealed: Base - def _(x: T | bool) -> None: + def _(x: T | Sub) -> None: reveal_type(x) # revealed: T - def _(x: T | None) -> None: - reveal_type(x) # revealed: T | None + def _(x: T | Unrelated) -> None: + reveal_type(x) # revealed: T | Unrelated def _(x: T | Any) -> None: reveal_type(x) # revealed: T | Any @@ -349,32 +396,44 @@ since there is no guarantee what type the typevar will be specialized to. from knot_extensions import Intersection from typing import Any +class Super: ... +class Base(Super): ... +class Sub(Base): ... +class Unrelated: ... + def unbounded_unconstrained[T](t: T) -> None: - def _(x: Intersection[T, int]) -> None: - reveal_type(x) # revealed: T & int + def _(x: Intersection[T, Super]) -> None: + reveal_type(x) # revealed: T & Super - def _(x: Intersection[T, bool]) -> None: - reveal_type(x) # revealed: T & bool + def _(x: Intersection[T, Base]) -> None: + reveal_type(x) # revealed: T & Base - def _(x: Intersection[T, None]) -> None: - reveal_type(x) # revealed: T & None + def _(x: Intersection[T, Sub]) -> None: + reveal_type(x) # revealed: T & Sub + + def _(x: Intersection[T, Unrelated]) -> None: + reveal_type(x) # revealed: T & Unrelated def _(x: Intersection[T, Any]) -> None: reveal_type(x) # revealed: T & Any ``` -The intersection of a bounded typevar with its bound is the typevar itself. (The typevar might be -specialized to a subtype of the bound.) The intersection of a bounded typevar with a subtype of its -bound cannot be simplified. (The typevar might be specialized to a different subtype of the bound.) -The intersection of a bounded typevar with a type that is disjoint from its bound is `Never`. +The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar +itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded +typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a +different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint +from its bound is `Never`. ```py -def bounded[T: int](t: T) -> None: - def _(x: Intersection[T, int]) -> None: +def bounded[T: Base](t: T) -> None: + def _(x: Intersection[T, Super]) -> None: + reveal_type(x) # revealed: T + + def _(x: Intersection[T, Base]) -> None: reveal_type(x) # revealed: T - def _(x: Intersection[T, bool]) -> None: - reveal_type(x) # revealed: T & bool + def _(x: Intersection[T, Sub]) -> None: + reveal_type(x) # revealed: T & Sub def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never @@ -383,21 +442,32 @@ def bounded[T: int](t: T) -> None: reveal_type(x) # revealed: T & Any ``` -When intersecting a constrained typevar with a type, we treat the typevar the same as a union of its -constraints. (TODO: This should be a union of "instances of _only_ this class", not "instances of -this class or a subclass".) +Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must +be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints, +since that would allow the typevar to take on values from _multiple_ constraints simultaneously. +The `OneOf` connector would not be a “type” according to a strict reading of the typing spec, since +it would not represent a single set of runtime objects; it would instead represent a _set of_ sets +of runtime objects. This is one reason we have not actually added this connector to our data model +yet. Nevertheless, describing constrained typevars this ways helps explain how we simplify +intersections involving them. + +This means that when intersecting a constrained typevar with a type `T`, constraints that are +supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover, +constraints that are disjoint from `T` are no longer valid specializations of the typevar, since +`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we +can simplify the intersection as a whole to that constraint. ```py -def constrained[T: (int, str, bool)](t: T) -> None: - def _(x: Intersection[T, int]) -> None: - # With OneOf this would be OneOf[int, bool] - reveal_type(x) # revealed: T & int +def constrained[T: (Base, Sub, Unrelated)](t: T) -> None: + def _(x: Intersection[T, Base]) -> None: + # With OneOf this would be OneOf[Base, Sub] + reveal_type(x) # revealed: T & Base - def _(x: Intersection[T, str]) -> None: - reveal_type(x) # revealed: str + def _(x: Intersection[T, Unrelated]) -> None: + reveal_type(x) # revealed: Unrelated - def _(x: Intersection[T, bool]) -> None: - reveal_type(x) # revealed: bool + def _(x: Intersection[T, Sub]) -> None: + reveal_type(x) # revealed: Sub def _(x: Intersection[T, None]) -> None: reveal_type(x) # revealed: Never @@ -406,8 +476,8 @@ def constrained[T: (int, str, bool)](t: T) -> None: reveal_type(x) # revealed: T & Any ``` -We do the same when removing a type from a constrained typevar, since this is modeled internally as -an intersection with a negation. +We can simplify the intersection similarly when removing a type from a constrained typevar, since +this is modeled internally as an intersection with a negation. ```py from knot_extensions import Not From 71d425e0d1548f984a60f764d036a3b3593b5330 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 19:41:55 -0400 Subject: [PATCH 028/100] Add multiple narrowing example --- .../resources/mdtest/generics/pep695.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 24126928e4101..483f7aaa51261 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -509,6 +509,7 @@ We can use narrowing expressions to eliminate some of the possibilities of a con ```py class P: ... class Q: ... +class R: ... def f[T: (P, Q)](t: T) -> None: if isinstance(t, P): @@ -524,6 +525,29 @@ def f[T: (P, Q)](t: T) -> None: else: reveal_type(t) # revealed: P p: P = t + +def g[T: (P, Q, R)](t: T) -> None: + if isinstance(t, P): + reveal_type(t) # revealed: P + p: P = t + elif isinstance(t, Q): + reveal_type(t) # revealed: Q & ~P + q: Q = t + else: + reveal_type(t) # revealed: R + r: R = t + + if isinstance(t, P): + reveal_type(t) # revealed: P + p: P = t + elif isinstance(t, Q): + reveal_type(t) # revealed: Q & ~P + q: Q = t + elif isinstance(t, R): + reveal_type(t) # revealed: R & ~P & ~Q + r: R = t + else: + reveal_type(t) # revealed: Never ``` [pep 695]: https://peps.python.org/pep-0695/ From 233e93807cc10f42bfb455c1504fc77e3b1792bc Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 19:51:13 -0400 Subject: [PATCH 029/100] lint --- .../resources/mdtest/generics/pep695.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 483f7aaa51261..f48f6a6813707 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -444,12 +444,12 @@ def bounded[T: Base](t: T) -> None: Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints, -since that would allow the typevar to take on values from _multiple_ constraints simultaneously. -The `OneOf` connector would not be a “type” according to a strict reading of the typing spec, since -it would not represent a single set of runtime objects; it would instead represent a _set of_ sets -of runtime objects. This is one reason we have not actually added this connector to our data model -yet. Nevertheless, describing constrained typevars this ways helps explain how we simplify -intersections involving them. +since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The +`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it +would not represent a single set of runtime objects; it would instead represent a _set of_ sets of +runtime objects. This is one reason we have not actually added this connector to our data model yet. +Nevertheless, describing constrained typevars this ways helps explain how we simplify intersections +involving them. This means that when intersecting a constrained typevar with a type `T`, constraints that are supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover, From 978520217417b6a9a62173e5a0f2a0d6eb8aaba9 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 19:51:15 -0400 Subject: [PATCH 030/100] Sort typevar constraints --- crates/red_knot_python_semantic/src/types.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 0fcb0bd545b8c..ca78c566646c5 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -624,8 +624,20 @@ impl<'db> Type<'db> { | Type::ClassLiteral(_) | Type::KnownInstance(_) | Type::IntLiteral(_) - | Type::SubclassOf(_) - | Type::TypeVar(_) => self, + | Type::SubclassOf(_) => self, + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(_)) | None => self, + Some(TypeVarBoundOrConstraints::Constraints(union)) => { + Type::TypeVar(TypeVarInstance::new( + db, + typevar.name(db).clone(), + Some(TypeVarBoundOrConstraints::Constraints( + union.to_sorted_union(db), + )), + typevar.default_ty(db), + )) + } + }, } } From c86af5021b151a3c6bcc0af82e88a882b28df1c8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 19:57:42 -0400 Subject: [PATCH 031/100] Remove moot todo --- crates/red_knot_python_semantic/src/types.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ca78c566646c5..da3c733a3ae6e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -673,8 +673,6 @@ impl<'db> Type<'db> { // A fully static typevar is a subtype of its upper bound, and to something similar to the // union of its constraints. An unbound, unconstrained, fully static typevar has an // implicit upper bound of `object` (which is handled below). - // TODO: It's not _really_ the union of its constraints because each occurrence of the - // typevar must be bound to the same type. if let Type::TypeVar(typevar) = self { match typevar.bound_or_constraints(db) { None => {} @@ -925,8 +923,6 @@ impl<'db> Type<'db> { // A typevar is assignable to its upper bound, and to something similar to the union of // its constraints. An unbound, unconstrained typevar has an implicit upper bound of // `object` (which is handled below). - // TODO: It's not _really_ the union of its constraints because each occurrence of the - // typevar must be bound to the same type. if let Type::TypeVar(typevar) = self { match typevar.bound_or_constraints(db) { None => {} From aa00895415b31606fc4b851928a36d88a6fcc8ef Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 20:53:47 -0400 Subject: [PATCH 032/100] Fold typevar match arms back into main match statement --- crates/red_knot_python_semantic/src/types.rs | 78 +++++++++----------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index da3c733a3ae6e..d88c7928f9d34 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -670,24 +670,6 @@ impl<'db> Type<'db> { return false; } - // A fully static typevar is a subtype of its upper bound, and to something similar to the - // union of its constraints. An unbound, unconstrained, fully static typevar has an - // implicit upper bound of `object` (which is handled below). - if let Type::TypeVar(typevar) = self { - match typevar.bound_or_constraints(db) { - None => {} - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - return bound.is_subtype_of(db, target); - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - return constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_subtype_of(db, target)); - } - } - } - match (self, target) { // We should have handled these immediately above. (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { @@ -711,6 +693,22 @@ impl<'db> Type<'db> { self_typevar == other_typevar } + // A fully static typevar is a subtype of its upper bound, and to something similar to + // the union of its constraints. An unbound, unconstrained, fully static typevar has an + // implicit upper bound of `object` (which is handled below). + (Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => { + match typevar.bound_or_constraints(db) { + None => unreachable!(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.is_subtype_of(db, target) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_subtype_of(db, target)), + } + } + (Type::Union(union), _) => union .elements(db) .iter() @@ -721,10 +719,6 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| self.is_subtype_of(db, elem_ty)), - // Other than the special cases enumerated above, typevars are never subtypes of any - // other variants. - (Type::TypeVar(_), _) => false, - (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { // No types are a subtype of a bounded typevar, or of an unbounded unconstrained // typevar, since there's no guarantee what type the typevar will be specialized @@ -906,9 +900,9 @@ impl<'db> Type<'db> { self_instance.is_subtype_of(db, target_instance) } - // Other than the special cases enumerated above, - // `Instance` types are never subtypes of any other variants - (Type::Instance(_), _) => false, + // Other than the special cases enumerated above, `Instance` types and typevars are + // never subtypes of any other variants + (Type::Instance(_) | Type::TypeVar(_), _) => false, } } @@ -920,24 +914,6 @@ impl<'db> Type<'db> { return true; } - // A typevar is assignable to its upper bound, and to something similar to the union of - // its constraints. An unbound, unconstrained typevar has an implicit upper bound of - // `object` (which is handled below). - if let Type::TypeVar(typevar) = self { - match typevar.bound_or_constraints(db) { - None => {} - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - return bound.is_assignable_to(db, target); - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - return constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_assignable_to(db, target)); - } - } - } - match (self, target) { // Never can be assigned to any type. (Type::Never, _) => true, @@ -958,6 +934,22 @@ impl<'db> Type<'db> { self_typevar == other_typevar } + // A typevar is assignable to its upper bound, and to something similar to the union of + // its constraints. An unbound, unconstrained typevar has an implicit upper bound of + // `object` (which is handled above). + (Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => { + match typevar.bound_or_constraints(db) { + None => unreachable!(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.is_assignable_to(db, target) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_assignable_to(db, target)), + } + } + // A union is assignable to a type T iff every element of the union is assignable to T. (Type::Union(union), ty) => union .elements(db) From d99d1d75f8fb727669fd52e56654309b527d3464 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 20:05:49 -0400 Subject: [PATCH 033/100] Remove moot comment --- crates/red_knot_python_semantic/src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d88c7928f9d34..2c6e58c9d3b26 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1538,7 +1538,6 @@ impl<'db> Type<'db> { pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { match self { Type::Dynamic(_) => false, - // A typevar is not fully static, since it can be specialized to a generic type Type::Never | Type::FunctionLiteral(..) | Type::BoundMethod(_) From 58f0995c51652e04be58c860ee76df209724ee26 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 20:07:08 -0400 Subject: [PATCH 034/100] Remove moot todo --- crates/red_knot_python_semantic/src/types.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 2c6e58c9d3b26..1173602b5b382 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3493,9 +3493,6 @@ impl<'db> Type<'db> { None => KnownClass::Object.to_class_literal(db), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - // TODO: This should create ClassLiterals, not SubclassOfTypes, for each - // element, since constraints must be specialized to those specific types, not - // to subclasses of those types. constraints.map(db, |constraint| constraint.to_meta_type(db)) } }, From 42fd54a5db1c55a4c5d3e385dda9a378dabac105 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 20:12:36 -0400 Subject: [PATCH 035/100] Add more TODOs about OneOf connector --- crates/red_knot_python_semantic/src/types.rs | 2 ++ crates/red_knot_python_semantic/src/types/infer.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1173602b5b382..e355fc6cde0ad 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3493,6 +3493,8 @@ impl<'db> Type<'db> { None => KnownClass::Object.to_class_literal(db), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // TODO: If we add a proper `OneOf` connector, we should use that here instead + // of union. (Using a union here doesn't break anything, but it is imprecise.) constraints.map(db, |constraint| constraint.to_meta_type(db)) } }, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index dfd506b7c80fc..050721b7cef08 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1974,6 +1974,8 @@ impl<'db> TypeInferenceBuilder<'db> { // We don't use UnionType::from_elements or UnionBuilder here, because we don't // want to simplify the list of constraints like we do with the elements of an // actual union type. + // TODO: Consider using a new `OneOfType` connective here instead, since that + // more accurately represents the actual semantics of typevar constraints. let elements = UnionType::new( self.db(), elts.iter() From 6bd69f18e49a19edd3a184db4ff929a238e11ac2 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 21:03:40 -0400 Subject: [PATCH 036/100] add todos for unary/binary ops --- crates/red_knot_python_semantic/src/types/infer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 050721b7cef08..13f732ae3a628 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4402,6 +4402,7 @@ impl<'db> TypeInferenceBuilder<'db> { match (op, operand_type) { (_, Type::Dynamic(_)) => operand_type, (_, Type::Never) => Type::Never, + // TODO: Apply the unary expression to the typevar's upper bound/constraints. (_, Type::TypeVar(_)) => Type::unknown(), (ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), @@ -4548,6 +4549,7 @@ impl<'db> TypeInferenceBuilder<'db> { (todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _) | (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), + // TODO: Apply the binary expression to the typevar's upper bound/constraints. (Type::TypeVar(_), _, _) | (_, Type::TypeVar(_), _) => Some(Type::unknown()), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( From 3869dc6892b58a55810d6cb8a9edc67dcab84cb0 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 21:15:23 -0400 Subject: [PATCH 037/100] Fix tests --- crates/red_knot_ide/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index 699b385d27c86..f3a8d8f12e382 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -160,6 +160,7 @@ impl HasNavigationTargets for Type<'_> { | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::PropertyInstance(_) + | Type::TypeVar(_) | Type::Tuple(_) => self.to_meta_type(db.upcast()).navigation_targets(db), Type::Intersection(intersection) => intersection.navigation_targets(db), From 0c1745bae1ba1be4fe04433b0caddda2661b4b42 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 2 Apr 2025 21:26:18 -0400 Subject: [PATCH 038/100] Fix tests better --- crates/red_knot_ide/src/lib.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index f3a8d8f12e382..39e12d64a8bca 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -160,9 +160,19 @@ impl HasNavigationTargets for Type<'_> { | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::PropertyInstance(_) - | Type::TypeVar(_) | Type::Tuple(_) => self.to_meta_type(db.upcast()).navigation_targets(db), + Type::TypeVar(var) => { + let definition = var.definition(db); + let full_range = definition.full_range(db.upcast()); + + NavigationTargets::single(NavigationTarget { + file: full_range.file(), + focus_range: definition.focus_range(db.upcast()).range(), + full_range: full_range.range(), + }) + } + Type::Intersection(intersection) => intersection.navigation_targets(db), Type::Dynamic(_) From 8bdd9e2bfb79c7a39be02baa2c84626061a96bd1 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 3 Apr 2025 10:18:18 -0400 Subject: [PATCH 039/100] Support unary and binary ops --- .../resources/mdtest/generics/functions.md | 4 +++- crates/red_knot_python_semantic/src/types/infer.rs | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index 55e61271e13db..32b4f1971b052 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -107,7 +107,7 @@ def good_return[T: int](x: T) -> T: return x def bad_return[T: int](x: T) -> T: - # TODO: error: int is not assignable to T + # error: [invalid-return-type] "Object of type `int` is not assignable to return type `T`" return x + 1 ``` @@ -137,6 +137,8 @@ methods that are compatible with the return type, so the `return` expression is ```py def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: + # TODO: no error + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`" return t1 + t2 ``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 61ac0397b6d13..80d895c6947e0 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4430,8 +4430,6 @@ impl<'db> TypeInferenceBuilder<'db> { match (op, operand_type) { (_, Type::Dynamic(_)) => operand_type, (_, Type::Never) => Type::Never, - // TODO: Apply the unary expression to the typevar's upper bound/constraints. - (_, Type::TypeVar(_)) => Type::unknown(), (ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), (ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), @@ -4472,7 +4470,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::SliceLiteral(_) - | Type::Tuple(_), + | Type::Tuple(_) + | Type::TypeVar(_), ) => { let unary_dunder_method = match op { ast::UnaryOp::Invert => "__invert__", @@ -4594,8 +4593,6 @@ impl<'db> TypeInferenceBuilder<'db> { (todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _) | (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), - // TODO: Apply the binary expression to the typevar's upper bound/constraints. - (Type::TypeVar(_), _, _) | (_, Type::TypeVar(_), _) => Some(Type::unknown()), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( n.checked_add(m) @@ -4749,7 +4746,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::SliceLiteral(_) - | Type::Tuple(_), + | Type::Tuple(_) + | Type::TypeVar(_), Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) @@ -4769,7 +4767,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::SliceLiteral(_) - | Type::Tuple(_), + | Type::Tuple(_) + | Type::TypeVar(_), op, ) => { // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from From 123b9209dd14d9691ffb2a176a7c7c3c4cfe4100 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 3 Apr 2025 11:26:53 -0400 Subject: [PATCH 040/100] Fix merge conflicts --- crates/red_knot_python_semantic/src/types.rs | 50 +++++++++++++------ .../src/types/class_base.rs | 4 ++ .../src/types/infer.rs | 19 +++---- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 0620244c7749b..1cc1e763356a1 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -389,6 +389,8 @@ impl<'db> Type<'db> { | Self::WrapperDescriptor(_) | Self::MethodWrapper(_) => false, + Self::Specialized(specialized) => specialized.callable_type(db).contains_todo(db), + Self::Callable(callable) => { let signature = callable.signature(db); signature.parameters().iter().any(|param| { @@ -619,6 +621,7 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), + Type::Specialized(specialized) => Type::Specialized(specialized.normalized(db)), Type::LiteralString | Type::Instance(_) | Type::PropertyInstance(_) @@ -763,7 +766,7 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), - (Type::Callable(CallableType::Specialized(specialized)), _) => { + (Type::Specialized(specialized), _) => { specialized.callable_type(db).is_subtype_of(db, target) } @@ -1193,7 +1196,6 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::Specialized(..) | Type::MethodWrapper(..) | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) @@ -1206,7 +1208,6 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::Specialized(..) | Type::MethodWrapper(..) | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) @@ -1392,10 +1393,13 @@ impl<'db> Type<'db> { .to_instance(db) .is_disjoint_from(db, other), - (Type::Specialized(specialized), Type::Instance(_)) - | (Type::Instance(_), Type::Specialized(specialized)) => { - specialized.callable_type(db).is_disjoint_from(db, other) + (Type::Specialized(self_specialized), Type::Specialized(other_specialized)) => { + self_specialized + .callable_type(db) + .is_disjoint_from(db, other_specialized.callable_type(db)) + || self_specialized.specialization(db) != other_specialized.specialization(db) } + (Type::Specialized(_), _) | (_, Type::Specialized(_)) => false, (Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => { KnownClass::MethodWrapperType @@ -1528,9 +1532,7 @@ impl<'db> Type<'db> { .elements(db) .iter() .all(|elem| elem.is_fully_static(db)), - Type::Specialized(specialized) => - specialized.callable_type(db).is_fully_static(db) - } + Type::Specialized(specialized) => specialized.callable_type(db).is_fully_static(db), Type::Callable(callable) => callable.is_fully_static(db), } } @@ -1578,9 +1580,7 @@ impl<'db> Type<'db> { // ``` false } - Type::Specialized(specialized) => { - specialized.callable_type(db).is_singleton(db) - } + Type::Specialized(specialized) => specialized.callable_type(db).is_singleton(db), Type::MethodWrapper(_) => { // Just a special case of `BoundMethod` really // (this variant represents `f.__get__`, where `f` is any function) @@ -1634,9 +1634,7 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, - Type::Callable(CallableType::Specialized(specialized)) => { - specialized.callable_type(db).is_single_valued(db) - } + Type::Specialized(specialized) => specialized.callable_type(db).is_single_valued(db), Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` @@ -1769,7 +1767,7 @@ impl<'db> Type<'db> { .find_name_in_mro(db, name) } - Type::Callable(CallableType::Specialized(specialized)) => { + Type::Specialized(specialized) => { // XXX: specialize the result specialized.callable_type(db).find_name_in_mro(db, name) } @@ -2441,6 +2439,10 @@ impl<'db> Type<'db> { Type::AlwaysFalsy => Truthiness::AlwaysFalse, + Type::Specialized(specialized) => specialized + .callable_type(db) + .try_bool_impl(db, allow_short_circuit)?, + Type::ClassLiteral(ClassLiteralType { class }) => class .metaclass_instance_type(db) .try_bool_impl(db, allow_short_circuit)?, @@ -3301,6 +3303,10 @@ impl<'db> Type<'db> { Some(builder.build()) } Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance()")), + Type::Specialized(specialized) => { + // XXX: specialize the result + specialized.callable_type(db).to_instance(db) + } Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::FunctionLiteral(_) @@ -3380,6 +3386,11 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }), + Type::Specialized(specialized) => { + // XXX: specialize the result + specialized.callable_type(db).in_type_expression(db) + } + Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), KnownInstanceType::Never | KnownInstanceType::NoReturn => Ok(Type::Never), @@ -4751,6 +4762,13 @@ pub struct SpecializedCallable<'db> { pub(crate) specialization: Specialization<'db>, } +impl<'db> SpecializedCallable<'db> { + fn normalized(self, db: &'db dyn Db) -> Self { + let callable_type = self.callable_type(db).normalized(db); + SpecializedCallable::new(db, callable_type, self.specialization(db)) + } +} + /// This type represents the set of all callable objects with a certain signature. /// It can be written in type expressions using `typing.Callable`. /// `lambda` expressions are inferred directly as `CallableType`s; all function-literal types diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 6309d6fb83175..63b313e708c04 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -87,6 +87,10 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => None, + Type::Specialized(specialized) => { + // XXX: Specialize the result + Self::try_from_type(db, specialized.callable_type(db)) + } Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 926cdd58f08ff..9ee6833eb8b5a 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -78,16 +78,13 @@ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType, - IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, - MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType, - Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, - TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, - UnionType, -}; -use crate::types::{ - CallableSignature, CallableType, FunctionDecorators, GeneralCallableType, Signature, Signatures, + todo_type, Class, DynamicType, FunctionType, IntersectionBuilder, IntersectionType, KnownClass, + KnownFunction, KnownInstanceType, MetaclassCandidate, Parameter, ParameterForm, Parameters, + SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, + TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarInstance, UnionBuilder, UnionType, }; +use crate::types::{CallableSignature, CallableType, FunctionDecorators, Signature, Signatures}; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; @@ -2357,6 +2354,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::FunctionLiteral(..) | Type::Callable(..) | Type::BoundMethod(_) + | Type::Specialized(_) | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::AlwaysTruthy @@ -4463,6 +4461,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::BoundMethod(_) + | Type::Specialized(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -4736,6 +4735,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) + | Type::Specialized(_) | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::ModuleLiteral(_) @@ -4756,6 +4756,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) + | Type::Specialized(_) | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::ModuleLiteral(_) From 9e1767fc765648656fe7eca1a36cf5dae3dcd779 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 3 Apr 2025 11:29:02 -0400 Subject: [PATCH 041/100] Rename to SpecializedCallable{,Type} --- crates/red_knot_python_semantic/src/types.rs | 51 ++++++++++++------- .../src/types/class_base.rs | 2 +- .../src/types/display.rs | 2 +- .../src/types/infer.rs | 8 +-- .../src/types/type_ordering.rs | 6 +-- 5 files changed, 41 insertions(+), 28 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1cc1e763356a1..58c35675c369f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -261,7 +261,7 @@ pub enum Type<'db> { /// Represents the specialization of a callable that has access to generic typevars, either /// because it is itself a generic function, or because it appears in the body of a generic /// class. - Specialized(SpecializedCallable<'db>), + SpecializedCallable(SpecializedCallableType<'db>), /// Represents a specific instance of `types.MethodWrapperType`. /// /// TODO: consider replacing this with `Callable & types.MethodWrapperType` type? @@ -389,7 +389,9 @@ impl<'db> Type<'db> { | Self::WrapperDescriptor(_) | Self::MethodWrapper(_) => false, - Self::Specialized(specialized) => specialized.callable_type(db).contains_todo(db), + Self::SpecializedCallable(specialized) => { + specialized.callable_type(db).contains_todo(db) + } Self::Callable(callable) => { let signature = callable.signature(db); @@ -621,7 +623,9 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), - Type::Specialized(specialized) => Type::Specialized(specialized.normalized(db)), + Type::SpecializedCallable(specialized) => { + Type::SpecializedCallable(specialized.normalized(db)) + } Type::LiteralString | Type::Instance(_) | Type::PropertyInstance(_) @@ -766,7 +770,7 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), - (Type::Specialized(specialized), _) => { + (Type::SpecializedCallable(specialized), _) => { specialized.callable_type(db).is_subtype_of(db, target) } @@ -1393,13 +1397,16 @@ impl<'db> Type<'db> { .to_instance(db) .is_disjoint_from(db, other), - (Type::Specialized(self_specialized), Type::Specialized(other_specialized)) => { + ( + Type::SpecializedCallable(self_specialized), + Type::SpecializedCallable(other_specialized), + ) => { self_specialized .callable_type(db) .is_disjoint_from(db, other_specialized.callable_type(db)) || self_specialized.specialization(db) != other_specialized.specialization(db) } - (Type::Specialized(_), _) | (_, Type::Specialized(_)) => false, + (Type::SpecializedCallable(_), _) | (_, Type::SpecializedCallable(_)) => false, (Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => { KnownClass::MethodWrapperType @@ -1532,7 +1539,9 @@ impl<'db> Type<'db> { .elements(db) .iter() .all(|elem| elem.is_fully_static(db)), - Type::Specialized(specialized) => specialized.callable_type(db).is_fully_static(db), + Type::SpecializedCallable(specialized) => { + specialized.callable_type(db).is_fully_static(db) + } Type::Callable(callable) => callable.is_fully_static(db), } } @@ -1580,7 +1589,9 @@ impl<'db> Type<'db> { // ``` false } - Type::Specialized(specialized) => specialized.callable_type(db).is_singleton(db), + Type::SpecializedCallable(specialized) => { + specialized.callable_type(db).is_singleton(db) + } Type::MethodWrapper(_) => { // Just a special case of `BoundMethod` really // (this variant represents `f.__get__`, where `f` is any function) @@ -1634,7 +1645,9 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, - Type::Specialized(specialized) => specialized.callable_type(db).is_single_valued(db), + Type::SpecializedCallable(specialized) => { + specialized.callable_type(db).is_single_valued(db) + } Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` @@ -1767,7 +1780,7 @@ impl<'db> Type<'db> { .find_name_in_mro(db, name) } - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: specialize the result specialized.callable_type(db).find_name_in_mro(db, name) } @@ -1851,7 +1864,7 @@ impl<'db> Type<'db> { Type::BoundMethod(_) => KnownClass::MethodType .to_instance(db) .instance_member(db, name), - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: specialize the result specialized.callable_type(db).instance_member(db, name) } @@ -2239,7 +2252,7 @@ impl<'db> Type<'db> { }) } }, - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: specialize the result specialized .callable_type(db) @@ -2439,7 +2452,7 @@ impl<'db> Type<'db> { Type::AlwaysFalsy => Truthiness::AlwaysFalse, - Type::Specialized(specialized) => specialized + Type::SpecializedCallable(specialized) => specialized .callable_type(db) .try_bool_impl(db, allow_short_circuit)?, @@ -3303,7 +3316,7 @@ impl<'db> Type<'db> { Some(builder.build()) } Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance()")), - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: specialize the result specialized.callable_type(db).to_instance(db) } @@ -3386,7 +3399,7 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }), - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: specialize the result specialized.callable_type(db).in_type_expression(db) } @@ -3589,7 +3602,7 @@ impl<'db> Type<'db> { Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: specialize the result specialized.callable_type(db).to_meta_type(db) } @@ -4753,7 +4766,7 @@ impl<'db> BoundMethodType<'db> { /// Represents the specialization of a callable that has access to generic typevars, either because /// it is itself a generic function, or because it appears in the body of a generic class. #[salsa::tracked(debug)] -pub struct SpecializedCallable<'db> { +pub struct SpecializedCallableType<'db> { /// The callable that has been specialized. (Note that this is not [`CallableType`] since there /// are other types that are callable.) pub(crate) callable_type: Type<'db>, @@ -4762,10 +4775,10 @@ pub struct SpecializedCallable<'db> { pub(crate) specialization: Specialization<'db>, } -impl<'db> SpecializedCallable<'db> { +impl<'db> SpecializedCallableType<'db> { fn normalized(self, db: &'db dyn Db) -> Self { let callable_type = self.callable_type(db).normalized(db); - SpecializedCallable::new(db, callable_type, self.specialization(db)) + SpecializedCallableType::new(db, callable_type, self.specialization(db)) } } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 63b313e708c04..122e95eb048b7 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -87,7 +87,7 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => None, - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { // XXX: Specialize the result Self::try_from_type(db, specialized.callable_type(db)) } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 68a3cd35e2a51..ba4cdd774f776 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -100,7 +100,7 @@ impl Display for DisplayRepresentation<'_> { instance = bound_method.self_instance(self.db).display(self.db) ) } - Type::Specialized(specialized) => { + Type::SpecializedCallable(specialized) => { write!( f, "", diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9ee6833eb8b5a..22e78bce3b682 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2354,7 +2354,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::FunctionLiteral(..) | Type::Callable(..) | Type::BoundMethod(_) - | Type::Specialized(_) + | Type::SpecializedCallable(_) | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::AlwaysTruthy @@ -4461,7 +4461,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::BoundMethod(_) - | Type::Specialized(_) + | Type::SpecializedCallable(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -4735,7 +4735,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::Specialized(_) + | Type::SpecializedCallable(_) | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::ModuleLiteral(_) @@ -4756,7 +4756,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::Specialized(_) + | Type::SpecializedCallable(_) | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::ModuleLiteral(_) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index fa9ad6c10f349..a8c538732dd72 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -65,9 +65,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::BoundMethod(_), _) => Ordering::Less, (_, Type::BoundMethod(_)) => Ordering::Greater, - (Type::Specialized(left), Type::Specialized(right)) => left.cmp(right), - (Type::Specialized(_), _) => Ordering::Less, - (_, Type::Specialized(_)) => Ordering::Greater, + (Type::SpecializedCallable(left), Type::SpecializedCallable(right)) => left.cmp(right), + (Type::SpecializedCallable(_), _) => Ordering::Less, + (_, Type::SpecializedCallable(_)) => Ordering::Greater, (Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right), (Type::MethodWrapper(_), _) => Ordering::Less, From 6b2794757d02ff4b9d2831eb08e0983c2a8315f8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 3 Apr 2025 13:43:16 -0400 Subject: [PATCH 042/100] Specialize types --- crates/red_knot_python_semantic/src/types.rs | 71 +++++++++++++++++++ .../src/types/generics.rs | 13 ++++ 2 files changed, 84 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 08f77e87ef6a1..05d01d0572d5b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3850,6 +3850,77 @@ impl<'db> Type<'db> { } } + /// Applies a specialization to this type, replacing any typevars with the types that they are + /// specialized to. + /// + /// Note that this does not specialize generic classes, functions, or type aliases! That is a + /// different operation that is performed explicitly (via a subscript operation), or implicitly + /// via a call to the generic object. + #[must_use] + #[salsa::tracked] + pub fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Type<'db> { + match self { + Type::TypeVar(typevar) => specialization.get(db, typevar).unwrap_or(self), + + // Callables need this extra wrapper that will apply the specialization to the + // callable's parameters and return types. That is done lazily if and when the callable + // is actually called. + Type::FunctionLiteral(_) + | Type::Callable(_) + | Type::BoundMethod(_) + | Type::SpecializedCallable(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) => { + Type::SpecializedCallable(SpecializedCallableType::new(db, self, specialization)) + } + + Type::Union(union) => union.map(db, |element| { + element.apply_specialization(db, specialization) + }), + Type::Intersection(intersection) => { + let mut builder = IntersectionBuilder::new(db); + for positive in intersection.positive(db) { + builder = + builder.add_positive(positive.apply_specialization(db, specialization)); + } + for negative in intersection.negative(db) { + builder = + builder.add_negative(negative.apply_specialization(db, specialization)); + } + builder.build() + } + Type::Tuple(tuple) => TupleType::from_elements( + db, + tuple + .iter(db) + .map(|ty| ty.apply_specialization(db, specialization)), + ), + + // XXX: Is this right? + Type::PropertyInstance(_) => self, + + Type::Dynamic(_) + | Type::Never + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::SubclassOf(_) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::LiteralString + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::SliceLiteral(_) + | Type::Instance(_) + | Type::KnownInstance(_) => self, + } + } + /// Return the string representation of this type when converted to string as it would be /// provided by the `__str__` method. /// diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 503d998836889..e3a3407c0d2ff 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -90,3 +90,16 @@ pub struct Specialization<'db> { generic_context: GenericContext<'db>, types: Box<[Type<'db>]>, } + +impl<'db> Specialization<'db> { + /// 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> { + self.generic_context(db) + .variables(db) + .iter() + .zip(self.types(db)) + .find(|(var, _)| **var == typevar) + .map(|(_, ty)| ty) + } +} From 575998ead51bf72f624fd1f6a2b6253f9dce199e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 3 Apr 2025 14:44:05 -0400 Subject: [PATCH 043/100] Fix tests in ide crate --- crates/red_knot_ide/src/lib.rs | 3 +++ crates/red_knot_python_semantic/src/types.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index 39e12d64a8bca..aa3019f023d56 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -137,6 +137,9 @@ impl HasNavigationTargets for Type<'_> { fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { match self { Type::BoundMethod(method) => method.function(db).navigation_targets(db), + Type::SpecializedCallable(specialized) => { + specialized.callable_type(db).navigation_targets(db) + } Type::FunctionLiteral(function) => function.navigation_targets(db), Type::ModuleLiteral(module) => module.navigation_targets(db), Type::Union(union) => union diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ee6aaf69c99d8..03a0cbda1bbcc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5066,10 +5066,10 @@ impl<'db> BoundMethodType<'db> { pub struct SpecializedCallableType<'db> { /// The callable that has been specialized. (Note that this is not [`CallableType`] since there /// are other types that are callable.) - pub(crate) callable_type: Type<'db>, + pub callable_type: Type<'db>, /// The specialization of any generic typevars that are visible to the callable. - pub(crate) specialization: Specialization<'db>, + pub specialization: Specialization<'db>, } impl<'db> SpecializedCallableType<'db> { From f02fefa0fa36d6c2e751843cd39ae07bcb52c9d1 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sun, 30 Mar 2025 14:33:57 -0400 Subject: [PATCH 044/100] Add GenericClass, NonGenericClass, and GenericAlias --- crates/red_knot_ide/src/lib.rs | 15 +- crates/red_knot_python_semantic/src/symbol.rs | 11 + crates/red_knot_python_semantic/src/types.rs | 108 ++- .../src/types/builder.rs | 8 +- .../src/types/call/bind.rs | 8 +- .../src/types/class.rs | 885 ++++++++++++------ .../src/types/class_base.rs | 54 +- .../src/types/diagnostic.rs | 4 +- .../src/types/display.rs | 8 +- .../src/types/generics.rs | 31 + .../src/types/infer.rs | 104 +- .../red_knot_python_semantic/src/types/mro.rs | 95 +- .../src/types/narrow.rs | 19 +- .../types/property_tests/type_generation.rs | 4 +- .../src/types/slots.rs | 14 +- .../src/types/subclass_of.rs | 4 +- .../src/types/type_ordering.rs | 10 +- 17 files changed, 883 insertions(+), 499 deletions(-) diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index aa3019f023d56..7883388332c5d 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut}; pub use db::Db; pub use goto::goto_type_definition; use red_knot_python_semantic::types::{ - Class, ClassBase, ClassLiteralType, FunctionType, InstanceType, IntersectionType, + ClassBase, ClassLiteralType, ClassType, FunctionType, InstanceType, IntersectionType, KnownInstanceType, ModuleLiteralType, Type, }; use ruff_db::files::{File, FileRange}; @@ -198,7 +198,7 @@ impl HasNavigationTargets for FunctionType<'_> { } } -impl HasNavigationTargets for Class<'_> { +impl HasNavigationTargets for ClassLiteralType<'_> { fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { let class_range = self.focus_range(db.upcast()); NavigationTargets::single(NavigationTarget { @@ -209,15 +209,20 @@ impl HasNavigationTargets for Class<'_> { } } -impl HasNavigationTargets for ClassLiteralType<'_> { +impl HasNavigationTargets for ClassType<'_> { fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { - self.class().navigation_targets(db) + let class_range = self.focus_range(db.upcast()); + NavigationTargets::single(NavigationTarget { + file: class_range.file(), + focus_range: class_range.range(), + full_range: self.full_range(db.upcast()).range(), + }) } } impl HasNavigationTargets for InstanceType<'_> { fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { - self.class().navigation_targets(db) + self.class.navigation_targets(db) } } diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs index e6a8fd39914e0..5740907bc4454 100644 --- a/crates/red_knot_python_semantic/src/symbol.rs +++ b/crates/red_knot_python_semantic/src/symbol.rs @@ -425,6 +425,17 @@ impl<'db> SymbolAndQualifiers<'db> { self.qualifiers.contains(TypeQualifiers::CLASS_VAR) } + #[must_use] + pub(crate) fn map_type( + self, + f: impl FnOnce(Type<'db>) -> Type<'db>, + ) -> SymbolAndQualifiers<'db> { + SymbolAndQualifiers { + symbol: self.symbol.map_type(f), + qualifiers: self.qualifiers, + } + } + /// Transform symbol and qualifiers into a [`LookupResult`], /// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol /// and the `Err` variant represents a symbol that is either definitely or possibly unbound. diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 03a0cbda1bbcc..e2bfe3ac9dbbb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -41,9 +41,8 @@ use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; use crate::{Db, FxOrderSet, Module, Program}; -pub use class::Class; -pub(crate) use class::KnownClass; -pub use class::{ClassLiteralType, InstanceType, KnownInstanceType}; +pub(crate) use class::{Class, GenericClass, KnownClass, NonGenericClass}; +pub use class::{ClassLiteralType, ClassType, InstanceType, KnownInstanceType}; mod builder; mod call; @@ -348,20 +347,17 @@ impl<'db> Type<'db> { fn is_none(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance().is_some_and(|instance| { - instance - .class() - .is_known(db, KnownClass::NotImplementedType) - }) + self.into_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) } pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class().is_object(db)) + .is_some_and(|instance| instance.class.is_object(db)) } pub const fn is_todo(&self) -> bool { @@ -437,10 +433,6 @@ impl<'db> Type<'db> { } } - pub const fn class_literal(class: Class<'db>) -> Self { - Self::ClassLiteral(ClassLiteralType { class }) - } - pub const fn into_class_literal(self) -> Option> { match self { Type::ClassLiteral(class_type) => Some(class_type), @@ -458,6 +450,27 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } + pub const fn into_class_type(self) -> Option> { + match self { + Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { + Some(ClassType::NonGeneric(non_generic)) + } + // XXX: GenericAlias + _ => None, + } + } + + #[track_caller] + pub fn expect_class_type(self) -> ClassType<'db> { + self.into_class_type() + .expect("Expected a Type::GenericAlias or non-generic Type::ClassLiteral variant") + } + + pub const fn is_class_type(&self) -> bool { + // XXX: GenericAlias + matches!(self, Type::ClassLiteral(ClassLiteralType::NonGeneric(_))) + } + pub const fn is_instance(&self) -> bool { matches!(self, Type::Instance(..)) } @@ -593,7 +606,7 @@ impl<'db> Type<'db> { matches!(self, Type::LiteralString) } - pub const fn instance(class: Class<'db>) -> Self { + pub const fn instance(class: ClassType<'db>) -> Self { Self::Instance(InstanceType { class }) } @@ -901,10 +914,7 @@ impl<'db> Type<'db> { // `Literal[]` is a subtype of `type[B]` if `C` is a subclass of `B`, // since `type[B]` describes all possible runtime subclasses of the class object `B`. - ( - Type::ClassLiteral(ClassLiteralType { class }), - Type::SubclassOf(target_subclass_ty), - ) => target_subclass_ty + (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() .is_some_and(|target_class| class.is_subclass_of(db, target_class)), @@ -917,7 +927,7 @@ impl<'db> Type<'db> { // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`. - (Type::ClassLiteral(ClassLiteralType { class }), _) => { + (Type::ClassLiteral(class), _) => { class.metaclass_instance_type(db).is_subtype_of(db, target) } @@ -1399,17 +1409,13 @@ impl<'db> Type<'db> { Type::Tuple(..), ) => true, - ( - Type::SubclassOf(subclass_of_ty), - Type::ClassLiteral(ClassLiteralType { class: class_b }), - ) - | ( - Type::ClassLiteral(ClassLiteralType { class: class_b }), - Type::SubclassOf(subclass_of_ty), - ) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => !class_b.is_subclass_of(db, class_a), - }, + (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) + | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { + match subclass_of_ty.subclass_of() { + ClassBase::Dynamic(_) => false, + ClassBase::Class(class_a) => !class_b.is_subclass_of(db, class_a), + } + } ( Type::SubclassOf(_), @@ -1526,12 +1532,10 @@ impl<'db> Type<'db> { // A class-literal type `X` is always disjoint from an instance type `Y`, // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, // where `Z` is `X`'s metaclass. - (Type::ClassLiteral(ClassLiteralType { class }), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::ClassLiteral(ClassLiteralType { class })) => { - !class - .metaclass_instance_type(db) - .is_subtype_of(db, instance) - } + (Type::ClassLiteral(class), instance @ Type::Instance(_)) + | (instance @ Type::Instance(_), Type::ClassLiteral(class)) => !class + .metaclass_instance_type(db) + .is_subtype_of(db, instance), (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => { @@ -1889,7 +1893,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Some(Symbol::bound(self).into()), - Type::ClassLiteral(class_literal @ ClassLiteralType { class }) => { + Type::ClassLiteral(class) => { match (class.known(db), name) { (Some(KnownClass::FunctionType), "__get__") => Some( Symbol::bound(Type::WrapperDescriptor( @@ -1933,7 +1937,7 @@ impl<'db> Type<'db> { "__get__" | "__set__" | "__delete__", ) => Some(Symbol::Unbound.into()), - _ => Some(class_literal.class_member(db, name)), + _ => Some(class.class_member(db, name)), } } @@ -2411,7 +2415,7 @@ impl<'db> Type<'db> { ) .into(), - Type::ClassLiteral(ClassLiteralType { class }) + Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => { Symbol::bound(Type::WrapperDescriptor( @@ -2419,7 +2423,7 @@ impl<'db> Type<'db> { )) .into() } - Type::ClassLiteral(ClassLiteralType { class }) + Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::Property) => { Symbol::bound(Type::WrapperDescriptor( @@ -2427,7 +2431,7 @@ impl<'db> Type<'db> { )) .into() } - Type::ClassLiteral(ClassLiteralType { class }) + Type::ClassLiteral(class) if name == "__set__" && class.is_known(db, KnownClass::Property) => { Symbol::bound(Type::WrapperDescriptor( @@ -2758,14 +2762,14 @@ impl<'db> Type<'db> { .callable_type(db) .try_bool_impl(db, allow_short_circuit)?, - Type::ClassLiteral(ClassLiteralType { class }) => class + Type::ClassLiteral(class) => class .metaclass_instance_type(db) .try_bool_impl(db, allow_short_circuit)?, Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { ClassBase::Dynamic(_) => Truthiness::Ambiguous, ClassBase::Class(class) => { - Type::class_literal(class).try_bool_impl(db, allow_short_circuit)? + Type::from(class).try_bool_impl(db, allow_short_circuit)? } }, @@ -3096,7 +3100,7 @@ impl<'db> Type<'db> { )), }, - Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) { + Type::ClassLiteral(class) => match class.known(db) { Some(KnownClass::Bool) => { // ```py // class bool(int): @@ -3278,7 +3282,7 @@ impl<'db> Type<'db> { Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db), - ClassBase::Class(class) => Type::class_literal(class).signatures(db), + ClassBase::Class(class) => Type::from(class).signatures(db), }, Type::Instance(_) => { @@ -3520,7 +3524,7 @@ impl<'db> Type<'db> { pub fn to_instance(&self, db: &'db dyn Db) -> Option> { match self { Type::Dynamic(_) | Type::Never => Some(*self), - Type::ClassLiteral(ClassLiteralType { class }) => Some(Type::instance(*class)), + Type::ClassLiteral(class) => Some(Type::instance(class.default_specialization(db))), Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()), Type::Union(union) => { let mut builder = UnionBuilder::new(db); @@ -3569,7 +3573,7 @@ impl<'db> Type<'db> { match self { // Special cases for `float` and `complex` // https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex - Type::ClassLiteral(ClassLiteralType { class }) => { + Type::ClassLiteral(class) => { let ty = match class.known(db) { Some(KnownClass::Any) => Type::any(), Some(KnownClass::Complex) => UnionType::from_elements( @@ -3587,7 +3591,7 @@ impl<'db> Type<'db> { KnownClass::Float.to_instance(db), ], ), - _ => Type::instance(*class), + _ => Type::instance(class.default_specialization(db)), }; Ok(ty) } @@ -3837,7 +3841,7 @@ impl<'db> Type<'db> { } }, - Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db), + Type::ClassLiteral(class) => class.metaclass(db), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { ClassBase::Dynamic(_) => *self, ClassBase::Class(class) => SubclassOfType::from( @@ -5732,8 +5736,8 @@ impl<'db> TypeAliasType<'db> { /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] pub(super) struct MetaclassCandidate<'db> { - metaclass: Class<'db>, - explicit_metaclass_of: Class<'db>, + metaclass: ClassType<'db>, + explicit_metaclass_of: ClassLiteralType<'db>, } #[salsa::interned(debug)] diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index ff03e4453aed2..4cbfbba1084ec 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -292,7 +292,7 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive .into_instance() - .and_then(|instance| instance.class().known(db)); + .and_then(|instance| instance.class.known(db)); if known_instance == Some(KnownClass::Object) { // `object & T` -> `T`; it is always redundant to add `object` to an intersection @@ -312,7 +312,7 @@ impl<'db> InnerIntersectionBuilder<'db> { new_positive = Type::BooleanLiteral(false); } Type::Instance(instance) - if instance.class().is_known(db, KnownClass::Bool) => + if instance.class.is_known(db, KnownClass::Bool) => { match new_positive { // `bool & AlwaysTruthy` -> `Literal[True]` @@ -406,7 +406,7 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive .iter() .filter_map(|ty| ty.into_instance()) - .filter_map(|instance| instance.class().known(db)) + .filter_map(|instance| instance.class.known(db)) .any(KnownClass::is_bool) }; @@ -422,7 +422,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::Instance(instance) if instance.class().is_object(db) => { + Type::Instance(instance) if instance.class.is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 8714e481208f8..1c823845f5633 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -18,8 +18,8 @@ use crate::types::diagnostic::{ }; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - todo_type, BoundMethodType, ClassLiteralType, FunctionDecorators, KnownClass, KnownFunction, - KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind, + todo_type, BoundMethodType, FunctionDecorators, KnownClass, KnownFunction, KnownInstanceType, + MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span}; use ruff_python_ast as ast; @@ -566,7 +566,7 @@ impl<'db> Bindings<'db> { _ => {} }, - Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) { + Type::ClassLiteral(class) => match class.known(db) { Some(KnownClass::Bool) => match overload.parameter_types() { [Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)), [None] => overload.set_return_type(Type::BooleanLiteral(false)), @@ -1058,7 +1058,7 @@ impl<'db> CallableDescription<'db> { }), Type::ClassLiteral(class_type) => Some(CallableDescription { kind: "class", - name: class_type.class().name(db), + name: class_type.name(db), }), Type::BoundMethod(bound_method) => Some(CallableDescription { kind: "bound method", diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 76d32a951e2e5..b8001ce365409 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -27,20 +27,16 @@ use super::{ KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType, TypeQualifiers, TypeVarInstance, }; -use crate::types::generics::GenericContext; +use crate::types::generics::{GenericContext, Specialization}; -/// Representation of a runtime class object. -/// -/// Does not in itself represent a type, -/// but is used as the inner data for several structs that *do* represent types. -#[salsa::interned(debug)] +/// Representation of a class definition statement in the AST. This does not in itself represent a +/// type, but is used as the inner data for several structs that *do* represent types. +#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)] pub struct Class<'db> { /// Name of the class at definition - #[return_ref] pub(crate) name: ast::name::Name, - pub(crate) generic_context: Option>, - body_scope: ScopeId<'db>, + pub(crate) body_scope: ScopeId<'db>, pub(crate) known: Option, } @@ -49,12 +45,15 @@ fn explicit_bases_cycle_recover<'db>( _db: &'db dyn Db, _value: &[Type<'db>], _count: u32, - _self: Class<'db>, + _self: ClassLiteralType<'db>, ) -> salsa::CycleRecoveryAction]>> { salsa::CycleRecoveryAction::Iterate } -fn explicit_bases_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Box<[Type<'db>]> { +fn explicit_bases_cycle_initial<'db>( + _db: &'db dyn Db, + _self: ClassLiteralType<'db>, +) -> Box<[Type<'db>]> { Box::default() } @@ -62,7 +61,7 @@ fn try_mro_cycle_recover<'db>( _db: &'db dyn Db, _value: &Result, MroError<'db>>, _count: u32, - _self: Class<'db>, + _self: ClassLiteralType<'db>, ) -> salsa::CycleRecoveryAction, MroError<'db>>> { salsa::CycleRecoveryAction::Iterate } @@ -70,9 +69,9 @@ fn try_mro_cycle_recover<'db>( #[allow(clippy::unnecessary_wraps)] fn try_mro_cycle_initial<'db>( db: &'db dyn Db, - self_: Class<'db>, + self_: ClassLiteralType<'db>, ) -> Result, MroError<'db>> { - Ok(Mro::from_error(db, self_)) + Ok(Mro::from_error(db, self_.default_specialization(db))) } #[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] @@ -80,20 +79,489 @@ fn inheritance_cycle_recover<'db>( _db: &'db dyn Db, _value: &Option, _count: u32, - _self: Class<'db>, + _self: ClassLiteralType<'db>, ) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } -fn inheritance_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Option { - None +fn inheritance_cycle_initial<'db>( + _db: &'db dyn Db, + _self: ClassLiteralType<'db>, +) -> Option { + None +} + +impl<'db> Class<'db> { + fn file(&self, db: &dyn Db) -> File { + self.body_scope.file(db) + } + + /// Return the original [`ast::StmtClassDef`] node associated with this class + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn node(&self, db: &'db dyn Db) -> &'db ast::StmtClassDef { + self.body_scope.node(db).expect_class() + } +} + +/// A [`Class`] that is not generic. +#[salsa::interned(debug)] +pub struct NonGenericClass<'db> { + #[return_ref] + pub(crate) class: Class<'db>, +} + +impl<'db> From> for Type<'db> { + fn from(class: NonGenericClass<'db>) -> Type<'db> { + Type::ClassLiteral(ClassLiteralType::NonGeneric(class)) + } +} + +/// A [`Class`] that is generic. +#[salsa::interned(debug)] +pub struct GenericClass<'db> { + #[return_ref] + pub(crate) class: Class<'db>, + pub(crate) generic_context: GenericContext<'db>, +} + +impl<'db> From> for Type<'db> { + fn from(class: GenericClass<'db>) -> Type<'db> { + Type::ClassLiteral(ClassLiteralType::Generic(class)) + } +} + +/// A specialization of a generic class with a particular assignment of types to typevars. +#[salsa::interned(debug)] +pub struct GenericAlias<'db> { + pub(crate) origin: GenericClass<'db>, + pub(crate) specialization: Specialization<'db>, +} + +impl<'db> From> for Type<'db> { + fn from(_alias: GenericAlias<'db>) -> Type<'db> { + // XXX: Type::GenericAlias(alias) + unreachable!() + } +} + +/* +impl<'db> GenericAlias<'db> { + /// Return an iterator over the inferred types of this generic alias's *explicit* bases, with + /// the alias's specialization applied. + pub(super) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { + self.explicit_bases_query(db) + } + + fn explicit_bases_query(self, db: &'db dyn Db) -> &'db [Type<'db>] { + // XXX: specialize result + ClassLiteralType::Generic(self).explicit_bases_query(db) + } +} +*/ + +/// Represents a class type, which might be a non-generic class, or a specialization of a generic +/// class. +#[derive( + Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update, +)] +pub enum ClassType<'db> { + NonGeneric(NonGenericClass<'db>), + Generic(GenericAlias<'db>), +} + +#[salsa::tracked] +impl<'db> ClassType<'db> { + fn class(self, db: &'db dyn Db) -> &'db Class<'db> { + match self { + Self::NonGeneric(non_generic) => non_generic.class(db), + Self::Generic(generic) => generic.origin(db).class(db), + } + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + &self.class(db).name + } + + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + self.class(db).known + } + + pub(crate) fn body_scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.class(db).body_scope + } + + /// Returns the file range of the class's name. + pub fn focus_range(self, db: &dyn Db) -> FileRange { + let class = self.class(db); + FileRange::new(class.file(db), class.node(db).name.range) + } + + pub fn full_range(self, db: &dyn Db) -> FileRange { + let class = self.class(db); + FileRange::new(class.file(db), class.node(db).range) + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => Self::Generic(GenericAlias::new( + db, + generic.origin(db), + generic + .specialization(db) + .apply_specialization(db, specialization), + )), + } + } + + fn specialize_class_base(&self, db: &'db dyn Db, base: ClassBase<'db>) -> ClassBase<'db> { + match self { + Self::NonGeneric(_) => base, + Self::Generic(generic) => base.apply_specialization(db, generic.specialization(db)), + } + } + + fn specialize_type(&self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + match self { + Self::NonGeneric(_) => ty, + Self::Generic(generic) => ty.apply_specialization(db, generic.specialization(db)), + } + } + + /// Return `true` if this class represents `known_class` + pub(crate) fn is_known(&self, db: &'db dyn Db, known_class: KnownClass) -> bool { + self.class(db).known == Some(known_class) + } + + /// Return `true` if this class represents the builtin class `object` + pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Object) + } + + /// Returns the class literal for this class. For a non-generic class, this is the class + /// itself. For a generic alias, this is the alias's origin. + pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> { + match self { + Self::NonGeneric(non_generic) => ClassLiteralType::NonGeneric(non_generic), + Self::Generic(generic) => ClassLiteralType::Generic(generic.origin(db)), + } + } + + /// Iterate over the [method resolution order] ("MRO") of the class. + /// + /// If the MRO could not be accurately resolved, this method falls back to iterating + /// over an MRO that has the class directly inheriting from `Unknown`. Use + /// [`Class::try_mro`] if you need to distinguish between the success and failure + /// cases rather than simply iterating over the inferred resolution order for the class. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(super) fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { + self.class_literal(db) + .iter_mro(db) + .map(move |base| self.specialize_class_base(db, base)) + } + + /// Is this class final? + pub(super) fn is_final(self, db: &'db dyn Db) -> bool { + self.class_literal(db).is_final(db) + } + + /// Return `true` if `other` is present in this class's MRO. + pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. + self.iter_mro(db).contains(&ClassBase::Class(other)) + } + + /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. + pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + self.specialize_type(db, self.class_literal(db).metaclass(db)) + } + + /// Return a type representing "the set of all instances of the metaclass of this class". + pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + self + .metaclass(db) + .to_instance(db) + .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + } + + /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. + pub(super) fn try_metaclass(self, db: &'db dyn Db) -> Result, MetaclassError<'db>> { + self.class_literal(db) + .try_metaclass(db) + .map(|ty| self.specialize_type(db, ty)) + } + + /// Returns the class member of this class named `name`. + /// + /// The member resolves to a member on the class itself or any of its proper superclasses. + /// + /// TODO: Should this be made private...? + pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + self.class_literal(db) + .class_member(db, name) + .map_type(|ty| self.specialize_type(db, ty)) + } + + /// Returns the inferred type of the class member named `name`. Only bound members + /// or those marked as ClassVars are considered. + /// + /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope + /// directly. Use [`Class::class_member`] if you require a method that will + /// traverse through the MRO until it finds the member. + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + self.class_literal(db) + .own_class_member(db, name) + .map_type(|ty| self.specialize_type(db, ty)) + } + + /// Returns the `name` attribute of an instance of this class. + /// + /// The attribute could be defined in the class body, but it could also be an implicitly + /// defined attribute that is only present in a method (typically `__init__`). + /// + /// The attribute might also be defined in a superclass of this class. + pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + self.class_literal(db) + .instance_member(db, name) + .map_type(|ty| self.specialize_type(db, ty)) + } + + /// Tries to find declarations/bindings of an instance attribute named `name` that are only + /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. + fn implicit_instance_attribute( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &str, + ) -> Option> { + // If we do not see any declarations of an attribute, neither in the class body nor in + // any method, we build a union of `Unknown` with the inferred types of all bindings of + // that attribute. We include `Unknown` in that union to account for the fact that the + // attribute might be externally modified. + let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); + + let attribute_assignments = attribute_assignments(db, class_body_scope); + + let attribute_assignments = attribute_assignments + .as_deref() + .and_then(|assignments| assignments.get(name))?; + + for attribute_assignment in attribute_assignments { + match attribute_assignment { + AttributeAssignment::Annotated { annotation } => { + // We found an annotated assignment of one of the following forms (using 'self' in these + // examples, but we support arbitrary names for the first parameters of methods): + // + // self.name: + // self.name: = … + + let annotation_ty = infer_expression_type(db, *annotation); + + // TODO: check if there are conflicting declarations + return Some(annotation_ty); + } + AttributeAssignment::Unannotated { value } => { + // We found an un-annotated attribute assignment of the form: + // + // self.name = + + let inferred_ty = infer_expression_type(db, *value); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + AttributeAssignment::Iterable { iterable } => { + // We found an attribute assignment like: + // + // for self.name in : + + let iterable_ty = infer_expression_type(db, *iterable); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = iterable_ty.iterate(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + AttributeAssignment::ContextManager { context_manager } => { + // We found an attribute assignment like: + // + // with as self.name: + + let context_ty = infer_expression_type(db, *context_manager); + let inferred_ty = context_ty.enter(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + AttributeAssignment::Unpack { + attribute_expression_id, + unpack, + } => { + // We found an unpacking assignment like: + // + // .., self.name, .. = + // (.., self.name, ..) = + // [.., self.name, ..] = + + let inferred_ty = + infer_unpack_types(db, *unpack).expression_type(*attribute_expression_id); + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + + Some(union_of_inferred_types.build()) + } + + /// A helper function for `instance_member` that looks up the `name` attribute only on + /// this class, not on its superclasses. + fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + // TODO: There are many things that are not yet implemented here: + // - `typing.Final` + // - Proper diagnostics + + let body_scope = self.body_scope(db); + let table = symbol_table(db, body_scope); + + if let Some(symbol_id) = table.symbol_id_by_name(name) { + let use_def = use_def_map(db, body_scope); + + let declarations = use_def.public_declarations(symbol_id); + let declared_and_qualifiers = symbol_from_declarations(db, declarations); + match declared_and_qualifiers { + Ok(SymbolAndQualifiers { + symbol: declared @ Symbol::Type(declared_ty, declaredness), + qualifiers, + }) => { + // The attribute is declared in the class body. + + let bindings = use_def.public_bindings(symbol_id); + let inferred = symbol_from_bindings(db, bindings); + let has_binding = !inferred.is_unbound(); + + if has_binding { + // The attribute is declared and bound in the class body. + + if let Some(implicit_ty) = + Self::implicit_instance_attribute(db, body_scope, name) + { + if declaredness == Boundness::Bound { + // If a symbol is definitely declared, and we see + // attribute assignments in methods of the class, + // we trust the declared type. + declared.with_qualifiers(qualifiers) + } else { + Symbol::Type( + UnionType::from_elements(db, [declared_ty, implicit_ty]), + declaredness, + ) + .with_qualifiers(qualifiers) + } + } else { + // The symbol is declared and bound in the class body, + // but we did not find any attribute assignments in + // methods of the class. This means that the attribute + // has a class-level default value, but it would not be + // found in a `__dict__` lookup. + + Symbol::Unbound.into() + } + } else { + // The attribute is declared but not bound in the class body. + // We take this as a sign that this is intended to be a pure + // instance attribute, and we trust the declared type, unless + // it is possibly-undeclared. In the latter case, we also + // union with the inferred type from attribute assignments. + + if declaredness == Boundness::Bound { + declared.with_qualifiers(qualifiers) + } else { + if let Some(implicit_ty) = + Self::implicit_instance_attribute(db, body_scope, name) + { + Symbol::Type( + UnionType::from_elements(db, [declared_ty, implicit_ty]), + declaredness, + ) + .with_qualifiers(qualifiers) + } else { + declared.with_qualifiers(qualifiers) + } + } + } + } + + Ok(SymbolAndQualifiers { + symbol: Symbol::Unbound, + qualifiers: _, + }) => { + // The attribute is not *declared* in the class body. It could still be declared/bound + // in a method. + + Self::implicit_instance_attribute(db, body_scope, name) + .map_or(Symbol::Unbound, Symbol::bound) + .into() + } + Err((declared, _conflicting_declarations)) => { + // There are conflicting declarations for this attribute in the class body. + Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) + } + } + } else { + // This attribute is neither declared nor bound in the class body. + // It could still be implicitly defined in a method. + + Self::implicit_instance_attribute(db, body_scope, name) + .map_or(Symbol::Unbound, Symbol::bound) + .into() + } + } +} + +impl<'db> From> for Type<'db> { + fn from(class: ClassType<'db>) -> Type<'db> { + match class { + ClassType::NonGeneric(non_generic) => non_generic.into(), + ClassType::Generic(generic) => generic.into(), + } + } +} + +/// Represents a single class object at runtime, which might be a non-generic class, or a generic +/// class that has not been specialized. +#[derive( + Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update, +)] +pub enum ClassLiteralType<'db> { + NonGeneric(NonGenericClass<'db>), + Generic(GenericClass<'db>), } #[salsa::tracked] -impl<'db> Class<'db> { +impl<'db> ClassLiteralType<'db> { + fn class(self, db: &'db dyn Db) -> &'db Class<'db> { + match self { + Self::NonGeneric(non_generic) => non_generic.class(db), + Self::Generic(generic) => generic.class(db), + } + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + &self.class(db).name + } + + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + self.class(db).known + } + /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.known(db) == Some(known_class) + pub(crate) fn is_known(&self, db: &'db dyn Db, known_class: KnownClass) -> bool { + self.class(db).known == Some(known_class) } /// Return `true` if this class represents the builtin class `object` @@ -101,6 +569,34 @@ impl<'db> Class<'db> { self.is_known(db, KnownClass::Object) } + pub(crate) fn body_scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.class(db).body_scope + } + + /// Returns the file range of the class's name. + pub fn focus_range(self, db: &dyn Db) -> FileRange { + let class = self.class(db); + FileRange::new(class.file(db), class.node(db).name.range) + } + + pub fn full_range(self, db: &dyn Db) -> FileRange { + let class = self.class(db); + FileRange::new(class.file(db), class.node(db).range) + } + + /// Returns the default specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// applies the default specialization to the class's typevars. + pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + match self { + Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), + Self::Generic(generic) => { + let specialization = generic.generic_context(db).default_specialization(db); + ClassType::Generic(GenericAlias::new(db, generic, specialization)) + } + } + } + /// Return an iterator over the inferred types of this class's *explicit* bases. /// /// Note that any class (except for `object`) that has no explicit @@ -119,21 +615,21 @@ impl<'db> Class<'db> { } /// Iterate over this class's explicit bases, filtering out any bases that are not class objects. - fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { self.explicit_bases(db) .iter() .copied() - .filter_map(Type::into_class_literal) - .map(|ClassLiteralType { class }| class) + .filter_map(Type::into_class_type) } #[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)] fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - tracing::trace!("Class::explicit_bases_query: {}", self.name(db)); + let class = self.class(db); + tracing::trace!("ClassLiteralType::explicit_bases_query: {}", class.name); - let class_stmt = self.node(db); + let class_stmt = class.node(db); let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + semantic_index(db, class.file(db)).expect_single_definition(class_stmt); class_stmt .bases() @@ -142,40 +638,19 @@ impl<'db> Class<'db> { .collect() } - fn file(self, db: &dyn Db) -> File { - self.body_scope(db).file(db) - } - - /// Return the original [`ast::StmtClassDef`] node associated with this class - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn node(self, db: &'db dyn Db) -> &'db ast::StmtClassDef { - self.body_scope(db).node(db).expect_class() - } - - /// Returns the file range of the class's name. - pub fn focus_range(self, db: &dyn Db) -> FileRange { - FileRange::new(self.file(db), self.node(db).name.range) - } - - pub fn full_range(self, db: &dyn Db) -> FileRange { - FileRange::new(self.file(db), self.node(db).range) - } - /// Return the types of the decorators on this class #[salsa::tracked(return_ref)] fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - tracing::trace!("Class::decorators: {}", self.name(db)); + let class = self.class(db); + tracing::trace!("ClassLiteralType::decorators: {}", class.name); - let class_stmt = self.node(db); + let class_stmt = class.node(db); if class_stmt.decorator_list.is_empty() { return Box::new([]); } let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + semantic_index(db, class.file(db)).expect_single_definition(class_stmt); class_stmt .decorator_list @@ -205,7 +680,8 @@ impl<'db> Class<'db> { /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order #[salsa::tracked(return_ref, cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial)] pub(super) fn try_mro(self, db: &'db dyn Db) -> Result, MroError<'db>> { - tracing::trace!("Class::try_mro: {}", self.name(db)); + let class = self.class(db); + tracing::trace!("ClassLiteralType::try_mro: {}", class.name); Mro::of_class(db, self) } @@ -222,7 +698,7 @@ impl<'db> Class<'db> { } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: Class) -> bool { + pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { // `is_subclass_of` is checking the subtype relation, in which gradual types do not // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. self.iter_mro(db).contains(&ClassBase::Class(other)) @@ -234,7 +710,8 @@ impl<'db> Class<'db> { /// Only call this function from queries in the same file or your /// query depends on the AST of another file (bad!). fn explicit_metaclass(self, db: &'db dyn Db) -> Option> { - let class_stmt = self.node(db); + let class = self.class(db); + let class_stmt = class.node(db); let metaclass_node = &class_stmt .arguments .as_ref()? @@ -242,7 +719,7 @@ impl<'db> Class<'db> { .value; let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + semantic_index(db, class.file(db)).expect_single_definition(class_stmt); Some(definition_expression_type( db, @@ -268,7 +745,8 @@ impl<'db> Class<'db> { /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. #[salsa::tracked] pub(super) fn try_metaclass(self, db: &'db dyn Db) -> Result, MetaclassError<'db>> { - tracing::trace!("Class::try_metaclass: {}", self.name(db)); + let class = self.class(db); + tracing::trace!("ClassLiteralType::try_metaclass: {}", class.name); // Identify the class's own metaclass (or take the first base class's metaclass). let mut base_classes = self.fully_static_explicit_bases(db).peekable(); @@ -284,18 +762,18 @@ impl<'db> Class<'db> { let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { (metaclass, self) } else if let Some(base_class) = base_classes.next() { - (base_class.metaclass(db), base_class) + (base_class.metaclass(db), base_class.class_literal(db)) } else { (KnownClass::Type.to_class_literal(db), self) }; - let mut candidate = if let Type::ClassLiteral(metaclass_ty) = metaclass { + let mut candidate = if let Some(metaclass_ty) = metaclass.into_class_type() { MetaclassCandidate { - metaclass: metaclass_ty.class, + metaclass: metaclass_ty, explicit_metaclass_of: class_metaclass_was_from, } } else { - let name = Type::string_literal(db, self.name(db)); + let name = Type::string_literal(db, &class.name); let bases = TupleType::from_elements(db, self.explicit_bases(db)); // TODO: Should be `dict[str, Any]` let namespace = KnownClass::Dict.to_instance(db); @@ -331,32 +809,32 @@ impl<'db> Class<'db> { // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 for base_class in base_classes { let metaclass = base_class.metaclass(db); - let Type::ClassLiteral(metaclass) = metaclass else { + let Some(metaclass) = metaclass.into_class_type() else { continue; }; - if metaclass.class.is_subclass_of(db, candidate.metaclass) { + if metaclass.is_subclass_of(db, candidate.metaclass) { candidate = MetaclassCandidate { - metaclass: metaclass.class, - explicit_metaclass_of: base_class, + metaclass, + explicit_metaclass_of: base_class.class_literal(db), }; continue; } - if candidate.metaclass.is_subclass_of(db, metaclass.class) { + if candidate.metaclass.is_subclass_of(db, metaclass) { continue; } return Err(MetaclassError { kind: MetaclassErrorKind::Conflict { candidate1: candidate, candidate2: MetaclassCandidate { - metaclass: metaclass.class, - explicit_metaclass_of: base_class, + metaclass, + explicit_metaclass_of: base_class.class_literal(db), }, candidate1_is_base_class: explicit_metaclass.is_none(), }, }); } - Ok(Type::class_literal(candidate.metaclass)) + Ok(candidate.metaclass.into()) } /// Returns the class member of this class named `name`. @@ -506,193 +984,6 @@ impl<'db> Class<'db> { } } - /// Tries to find declarations/bindings of an instance attribute named `name` that are only - /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. - fn implicit_instance_attribute( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: &str, - ) -> Option> { - // If we do not see any declarations of an attribute, neither in the class body nor in - // any method, we build a union of `Unknown` with the inferred types of all bindings of - // that attribute. We include `Unknown` in that union to account for the fact that the - // attribute might be externally modified. - let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); - - let attribute_assignments = attribute_assignments(db, class_body_scope); - - let attribute_assignments = attribute_assignments - .as_deref() - .and_then(|assignments| assignments.get(name))?; - - for attribute_assignment in attribute_assignments { - match attribute_assignment { - AttributeAssignment::Annotated { annotation } => { - // We found an annotated assignment of one of the following forms (using 'self' in these - // examples, but we support arbitrary names for the first parameters of methods): - // - // self.name: - // self.name: = … - - let annotation_ty = infer_expression_type(db, *annotation); - - // TODO: check if there are conflicting declarations - return Some(annotation_ty); - } - AttributeAssignment::Unannotated { value } => { - // We found an un-annotated attribute assignment of the form: - // - // self.name = - - let inferred_ty = infer_expression_type(db, *value); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - AttributeAssignment::Iterable { iterable } => { - // We found an attribute assignment like: - // - // for self.name in : - - let iterable_ty = infer_expression_type(db, *iterable); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = iterable_ty.iterate(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - AttributeAssignment::ContextManager { context_manager } => { - // We found an attribute assignment like: - // - // with as self.name: - - let context_ty = infer_expression_type(db, *context_manager); - let inferred_ty = context_ty.enter(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - AttributeAssignment::Unpack { - attribute_expression_id, - unpack, - } => { - // We found an unpacking assignment like: - // - // .., self.name, .. = - // (.., self.name, ..) = - // [.., self.name, ..] = - - let inferred_ty = - infer_unpack_types(db, *unpack).expression_type(*attribute_expression_id); - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - - Some(union_of_inferred_types.build()) - } - - /// A helper function for `instance_member` that looks up the `name` attribute only on - /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - // TODO: There are many things that are not yet implemented here: - // - `typing.Final` - // - Proper diagnostics - - let body_scope = self.body_scope(db); - let table = symbol_table(db, body_scope); - - if let Some(symbol_id) = table.symbol_id_by_name(name) { - let use_def = use_def_map(db, body_scope); - - let declarations = use_def.public_declarations(symbol_id); - let declared_and_qualifiers = symbol_from_declarations(db, declarations); - match declared_and_qualifiers { - Ok(SymbolAndQualifiers { - symbol: declared @ Symbol::Type(declared_ty, declaredness), - qualifiers, - }) => { - // The attribute is declared in the class body. - - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); - let has_binding = !inferred.is_unbound(); - - if has_binding { - // The attribute is declared and bound in the class body. - - if let Some(implicit_ty) = - Self::implicit_instance_attribute(db, body_scope, name) - { - if declaredness == Boundness::Bound { - // If a symbol is definitely declared, and we see - // attribute assignments in methods of the class, - // we trust the declared type. - declared.with_qualifiers(qualifiers) - } else { - Symbol::Type( - UnionType::from_elements(db, [declared_ty, implicit_ty]), - declaredness, - ) - .with_qualifiers(qualifiers) - } - } else { - // The symbol is declared and bound in the class body, - // but we did not find any attribute assignments in - // methods of the class. This means that the attribute - // has a class-level default value, but it would not be - // found in a `__dict__` lookup. - - Symbol::Unbound.into() - } - } else { - // The attribute is declared but not bound in the class body. - // We take this as a sign that this is intended to be a pure - // instance attribute, and we trust the declared type, unless - // it is possibly-undeclared. In the latter case, we also - // union with the inferred type from attribute assignments. - - if declaredness == Boundness::Bound { - declared.with_qualifiers(qualifiers) - } else { - if let Some(implicit_ty) = - Self::implicit_instance_attribute(db, body_scope, name) - { - Symbol::Type( - UnionType::from_elements(db, [declared_ty, implicit_ty]), - declaredness, - ) - .with_qualifiers(qualifiers) - } else { - declared.with_qualifiers(qualifiers) - } - } - } - } - - Ok(SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers: _, - }) => { - // The attribute is not *declared* in the class body. It could still be declared/bound - // in a method. - - Self::implicit_instance_attribute(db, body_scope, name) - .map_or(Symbol::Unbound, Symbol::bound) - .into() - } - Err((declared, _conflicting_declarations)) => { - // There are conflicting declarations for this attribute in the class body. - Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) - } - } - } else { - // This attribute is neither declared nor bound in the class body. - // It could still be implicitly defined in a method. - - Self::implicit_instance_attribute(db, body_scope, name) - .map_or(Symbol::Unbound, Symbol::bound) - .into() - } - } - /// Return this class' involvement in an inheritance cycle, if any. /// /// A class definition like this will fail at runtime, @@ -704,21 +995,22 @@ impl<'db> Class<'db> { /// Also, populates `visited_classes` with all base classes of `self`. fn is_cyclically_defined_recursive<'db>( db: &'db dyn Db, - class: Class<'db>, - classes_on_stack: &mut IndexSet>, - visited_classes: &mut IndexSet>, + class: ClassLiteralType<'db>, + classes_on_stack: &mut IndexSet>, + visited_classes: &mut IndexSet>, ) -> bool { let mut result = false; for explicit_base_class in class.fully_static_explicit_bases(db) { - if !classes_on_stack.insert(explicit_base_class) { + let explicit_base_class_literal = explicit_base_class.class_literal(db); + if !classes_on_stack.insert(explicit_base_class_literal) { return true; } - if visited_classes.insert(explicit_base_class) { + if visited_classes.insert(explicit_base_class_literal) { // If we find a cycle, keep searching to check if we can reach the starting class. result |= is_cyclically_defined_recursive( db, - explicit_base_class, + explicit_base_class_literal, classes_on_stack, visited_classes, ); @@ -742,6 +1034,15 @@ impl<'db> Class<'db> { } } +impl<'db> From> for Type<'db> { + fn from(class: ClassLiteralType<'db>) -> Type<'db> { + match class { + ClassLiteralType::NonGeneric(non_generic) => non_generic.into(), + ClassLiteralType::Generic(generic) => generic.into(), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(super) enum InheritanceCycle { /// The class is cyclically defined and is a participant in the cycle. @@ -758,43 +1059,13 @@ impl InheritanceCycle { } } -/// A singleton type representing a single class object at runtime. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub struct ClassLiteralType<'db> { - pub(super) class: Class<'db>, -} - -impl<'db> ClassLiteralType<'db> { - pub fn class(self) -> Class<'db> { - self.class - } - - pub(crate) fn body_scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.class.body_scope(db) - } - - pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.class.class_member(db, name) - } -} - -impl<'db> From> for Type<'db> { - fn from(value: ClassLiteralType<'db>) -> Self { - Self::ClassLiteral(value) - } -} - /// A type representing the set of runtime objects which are instances of a certain class. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] pub struct InstanceType<'db> { - pub(super) class: Class<'db>, + pub class: ClassType<'db>, } impl<'db> InstanceType<'db> { - pub fn class(self) -> Class<'db> { - self.class - } - pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { // N.B. The subclass relation is fully static self.class.is_subclass_of(db, other.class) @@ -814,7 +1085,7 @@ impl<'db> From> for Type<'db> { /// Note: good candidates are any classes in `[crate::module_resolver::module::KnownModule]` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(test, derive(strum_macros::EnumIter))] -pub enum KnownClass { +pub(crate) enum KnownClass { // To figure out where an stdlib symbol is defined, you can go into `crates/red_knot_vendored` // and grep for the symbol name in any `.pyi` file. @@ -1056,8 +1327,8 @@ impl<'db> KnownClass { /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) - .into_class_literal() - .map(|ClassLiteralType { class }| Type::instance(class)) + .into_class_type() + .map(|class| Type::instance(class)) .unwrap_or_else(Type::unknown) } @@ -1071,9 +1342,9 @@ impl<'db> KnownClass { ) -> Result, KnownClassLookupError<'db>> { let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).symbol; match symbol { - Symbol::Type(Type::ClassLiteral(class_type), Boundness::Bound) => Ok(class_type), - Symbol::Type(Type::ClassLiteral(class_type), Boundness::PossiblyUnbound) => { - Err(KnownClassLookupError::ClassPossiblyUnbound { class_type }) + Symbol::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal), + Symbol::Type(Type::ClassLiteral(class_literal), Boundness::PossiblyUnbound) => { + Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }) } Symbol::Type(found_type, _) => { Err(KnownClassLookupError::SymbolNotAClass { found_type }) @@ -1108,8 +1379,8 @@ impl<'db> KnownClass { } match lookup_error { - KnownClassLookupError::ClassPossiblyUnbound { class_type, .. } => { - Type::class_literal(class_type.class) + KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { + class_literal.into() } KnownClassLookupError::ClassNotFound { .. } | KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(), @@ -1123,16 +1394,16 @@ impl<'db> KnownClass { /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) - .into_class_literal() - .map(|ClassLiteralType { class }| SubclassOfType::from(db, class)) + .into_class_type() + .map(|class| SubclassOfType::from(db, class)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, /// *and* `class` is a subclass of `other`. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: Class<'db>) -> bool { + pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { self.try_to_class_literal(db) - .is_ok_and(|ClassLiteralType { class }| class.is_subclass_of(db, other)) + .is_ok_and(|class| class.is_subclass_of(db, other)) } /// Return the module in which we should look up the definition for this class @@ -1464,7 +1735,9 @@ pub(crate) enum KnownClassLookupError<'db> { SymbolNotAClass { found_type: Type<'db> }, /// There is a symbol by that name in the expected typeshed module, /// and it's a class definition, but it's possibly unbound. - ClassPossiblyUnbound { class_type: ClassLiteralType<'db> }, + ClassPossiblyUnbound { + class_literal: ClassLiteralType<'db>, + }, } impl<'db> KnownClassLookupError<'db> { @@ -1744,7 +2017,7 @@ impl<'db> KnownInstanceType<'db> { } /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &'db dyn Db, class: Class<'db>) -> bool { + pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool { self.class().is_subclass_of(db, class) } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 54179bfbcb721..779e95800d60d 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -1,16 +1,20 @@ -use crate::types::{todo_type, Class, DynamicType, KnownClass, KnownInstanceType, Type}; +use crate::types::generics::Specialization; +use crate::types::{todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, Type}; use crate::Db; use itertools::Either; /// Enumeration of the possible kinds of types we allow in class bases. /// -/// This is much more limited than the [`Type`] enum: -/// all types that would be invalid to have as a class base are -/// transformed into [`ClassBase::unknown`] +/// This is much more limited than the [`Type`] enum: all types that would be invalid to have as a +/// class base are transformed into [`ClassBase::unknown`] +/// +/// Note that a non-specialized generic class _cannot_ be a class base. When we see a +/// non-specialized generic class in any type expression (including the list of base classes), we +/// automatically construct the default specialization for that class. #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)] pub enum ClassBase<'db> { Dynamic(DynamicType), - Class(Class<'db>), + Class(ClassType<'db>), } impl<'db> ClassBase<'db> { @@ -29,6 +33,30 @@ impl<'db> ClassBase<'db> { } } + /// Applies a specialization to this class base. This is used, for instance, when a generic + /// class inherits from a generic alias: + /// + /// ```py + /// class A[T]: ... + /// class B[U](A[U]): ... + /// ``` + /// + /// `B` is a generic class, whose MRO includes the generic alias `A[U]`. If `B` is specialized + /// to `B[int]`, with specialization `{U: int}`, we can apply that specialization to the base + /// class. That results in `A[int]`, which is the corresponding entry in the MRO of `B[int]`. + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + match self { + ClassBase::Dynamic(_) => self, + ClassBase::Class(class) => { + ClassBase::Class(class.apply_specialization(db, specialization)) + } + } + } + pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct Display<'db> { base: ClassBase<'db>, @@ -51,8 +79,8 @@ impl<'db> ClassBase<'db> { pub(super) fn object(db: &'db dyn Db) -> Self { KnownClass::Object .to_class_literal(db) - .into_class_literal() - .map_or(Self::unknown(), |literal| Self::Class(literal.class())) + .into_class_type() + .map_or(Self::unknown(), |class| Self::Class(class)) } /// Attempt to resolve `ty` into a `ClassBase`. @@ -61,10 +89,10 @@ impl<'db> ClassBase<'db> { pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { match ty { Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), - Type::ClassLiteral(literal) => Some(if literal.class().is_known(db, KnownClass::Any) { + Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) { Self::Dynamic(DynamicType::Any) } else { - Self::Class(literal.class()) + Self::Class(literal.default_specialization(db)) }), Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? @@ -163,7 +191,7 @@ impl<'db> ClassBase<'db> { } } - pub(super) fn into_class(self) -> Option> { + pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), Self::Dynamic(_) => None, @@ -182,8 +210,8 @@ impl<'db> ClassBase<'db> { } } -impl<'db> From> for ClassBase<'db> { - fn from(value: Class<'db>) -> Self { +impl<'db> From> for ClassBase<'db> { + fn from(value: ClassType<'db>) -> Self { ClassBase::Class(value) } } @@ -192,7 +220,7 @@ impl<'db> From> for Type<'db> { fn from(value: ClassBase<'db>) -> Self { match value { ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), - ClassBase::Class(class) => Type::class_literal(class), + ClassBase::Class(class) => class.into(), } } } diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 42fba177dc49f..d57f8c896b5d5 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -7,7 +7,7 @@ use crate::types::string_annotation::{ IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, }; -use crate::types::{ClassLiteralType, KnownInstanceType, Type}; +use crate::types::{KnownInstanceType, Type}; use ruff_db::diagnostic::{Diagnostic, OldSecondaryDiagnosticMessage, Span}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::Ranged; @@ -1025,7 +1025,7 @@ fn report_invalid_assignment_with_message( message: std::fmt::Arguments, ) { match target_ty { - Type::ClassLiteral(ClassLiteralType { class }) => { + Type::ClassLiteral(class) => { context.report_lint(&INVALID_ASSIGNMENT, node, format_args!( "Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional", class.name(context.db()))); diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index d1f3c1ccf9dc9..8a1af518d4726 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -9,8 +9,8 @@ use ruff_python_literal::escape::AsciiEscape; use crate::types::class_base::ClassBase; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - ClassLiteralType, InstanceType, IntersectionType, KnownClass, MethodWrapperKind, - StringLiteralType, Type, UnionType, WrapperDescriptorKind, + InstanceType, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, Type, + UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -34,7 +34,7 @@ impl Display for DisplayType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let representation = self.ty.representation(self.db); match self.ty { - Type::ClassLiteral(literal) if literal.class().is_known(self.db, KnownClass::Any) => { + Type::ClassLiteral(literal) if literal.is_known(self.db, KnownClass::Any) => { write!(f, "typing.Any") } Type::IntLiteral(_) @@ -82,7 +82,7 @@ impl Display for DisplayRepresentation<'_> { write!(f, "", module.module(self.db).name()) } // TODO functions and classes should display using a fully qualified name - Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)), + Type::ClassLiteral(class) => f.write_str(class.name(self.db)), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { // Only show the bare class name here; ClassBase::display would render this as // type[] instead of type[Foo]. diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index e3a3407c0d2ff..87f52f28e0a0f 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -75,6 +75,15 @@ impl<'db> GenericContext<'db> { parameter } + 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) + } + pub(crate) fn specialize( self, db: &'db dyn Db, @@ -92,6 +101,28 @@ pub struct Specialization<'db> { } impl<'db> Specialization<'db> { + /// Applies a specialization to this specialization. This is used, for instance, when a generic + /// class inherits from a generic alias: + /// + /// ```py + /// class A[T]: ... + /// class B[U](A[U]): ... + /// ``` + /// + /// `B` is a generic class, whose MRO includes the generic alias `A[U]`, which specializes `A` + /// with the specialization `{T: U}`. If `B` is specialized to `B[int]`, with specialization + /// `{U: int}`, we can apply the second specialization to the first, resulting in `T: int`. + /// 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 { + let types = self + .types(db) + .iter() + .map(|ty| ty.apply_specialization(db, other)) + .collect(); + Specialization::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> { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 022d2c4401896..b6d19e3bb7524 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -61,7 +61,7 @@ use crate::symbol::{ typing_extensions_symbol, Boundness, LookupError, }; use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError}; -use crate::types::class::{ClassLiteralType, MetaclassErrorKind}; +use crate::types::class::MetaclassErrorKind; use crate::types::diagnostic::{ report_implicit_return_type, report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, report_invalid_assignment, @@ -78,10 +78,11 @@ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - todo_type, Class, DynamicType, FunctionType, IntersectionBuilder, IntersectionType, KnownClass, - KnownFunction, KnownInstanceType, MetaclassCandidate, Parameter, ParameterForm, Parameters, - SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, - TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, + todo_type, Class, ClassLiteralType, DynamicType, FunctionType, GenericClass, + IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, + MetaclassCandidate, NonGenericClass, Parameter, ParameterForm, Parameters, SliceLiteralType, + SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, + TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; use crate::types::{CallableSignature, CallableType, FunctionDecorators, Signature, Signatures}; @@ -708,7 +709,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let DefinitionKind::Class(class) = definition.kind(self.db()) { ty.inner_type() .into_class_literal() - .map(|ty| (ty.class(), class.node())) + .map(|ty| (ty, class.node())) } else { None } @@ -736,10 +737,7 @@ impl<'db> TypeInferenceBuilder<'db> { // (2) Check for classes that inherit from `@final` classes for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { // dynamic/unknown bases are never `@final` - let Some(base_class) = base_class - .into_class_literal() - .map(super::class::ClassLiteralType::class) - else { + let Some(base_class) = base_class.into_class_literal() else { continue; }; if !base_class.is_final(self.db()) { @@ -983,7 +981,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::Instance(instance) if matches!( - instance.class().known(self.db()), + instance.class.known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) ) => {} _ => return false, @@ -1394,7 +1392,7 @@ impl<'db> TypeInferenceBuilder<'db> { continue; } } else if let Type::ClassLiteral(class) = decorator_ty { - if class.class.is_known(self.db(), KnownClass::Classmethod) { + if class.is_known(self.db(), KnownClass::Classmethod) { function_decorators |= FunctionDecorators::CLASSMETHOD; continue; } @@ -1683,14 +1681,18 @@ impl<'db> TypeInferenceBuilder<'db> { let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name); - let class = Class::new( - self.db(), - &name.id, - generic_context, + let class = Class { + name: name.id.clone(), body_scope, - maybe_known_class, - ); - let class_ty = Type::class_literal(class); + known: maybe_known_class, + }; + let class_literal = match generic_context { + Some(generic_context) => { + ClassLiteralType::Generic(GenericClass::new(self.db(), class, generic_context)) + } + None => ClassLiteralType::NonGeneric(NonGenericClass::new(self.db(), class)), + }; + let class_ty = Type::from(class_literal); self.add_declaration_with_binding( class_node.into(), @@ -2805,10 +2807,7 @@ impl<'db> TypeInferenceBuilder<'db> { // Handle various singletons. if let Type::Instance(instance) = declared_ty.inner_type() { - if instance - .class() - .is_known(self.db(), KnownClass::SpecialForm) - { + if instance.class.is_known(self.db(), KnownClass::SpecialForm) { if let Some(name_expr) = target.as_name_expr() { if let Some(known_instance) = KnownInstanceType::try_from_file_and_name( self.db(), @@ -4362,7 +4361,7 @@ impl<'db> TypeInferenceBuilder<'db> { LookupError::Unbound(_) => { let bound_on_instance = match value_type { Type::ClassLiteral(class) => { - !class.class().instance_member(db, attr).symbol.is_unbound() + !class.instance_member(db, attr).symbol.is_unbound() } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { @@ -5342,9 +5341,7 @@ impl<'db> TypeInferenceBuilder<'db> { range, ), (Type::Tuple(_), Type::Instance(instance)) - if instance - .class() - .is_known(self.db(), KnownClass::VersionInfo) => + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( left, @@ -5354,9 +5351,7 @@ impl<'db> TypeInferenceBuilder<'db> { ) } (Type::Instance(instance), Type::Tuple(_)) - if instance - .class() - .is_known(self.db(), KnownClass::VersionInfo) => + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( Type::version_info_tuple(self.db()), @@ -5671,15 +5666,13 @@ impl<'db> TypeInferenceBuilder<'db> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. let value_ty = self.infer_expression(value); - if let Type::ClassLiteral(ClassLiteralType { class }) = value_ty { - if let Some(generic_context) = class.generic_context(self.db()) { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - generic_context, - slice, - ); - } + if let Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) = value_ty { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + generic_class.generic_context(self.db()), + slice, + ); } let slice_ty = self.infer_expression(slice); @@ -5740,16 +5733,12 @@ impl<'db> TypeInferenceBuilder<'db> { ( Type::Instance(instance), Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_), - ) if instance - .class() - .is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_subscript_expression_types( + ) if instance.class.is_known(self.db(), KnownClass::VersionInfo) => self + .infer_subscript_expression_types( value_node, Type::version_info_tuple(self.db()), slice_ty, - ) - } + ), // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => { @@ -5957,12 +5946,12 @@ impl<'db> TypeInferenceBuilder<'db> { } } - if let Type::ClassLiteral(ClassLiteralType { class }) = value_ty { + if let Type::ClassLiteral(class) = value_ty { if class.is_known(self.db(), KnownClass::Type) { return KnownClass::GenericAlias.to_instance(self.db()); } - if class.generic_context(self.db()).is_some() { + if let ClassLiteralType::Generic(_) = class { // TODO: specialize the generic class using these explicit type // variable assignments return value_ty; @@ -6025,7 +6014,7 @@ impl<'db> TypeInferenceBuilder<'db> { }, Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), Some(Type::Instance(instance)) - if instance.class().is_known(self.db(), KnownClass::NoneType) => + if instance.class.is_known(self.db(), KnownClass::NoneType) => { SliceArg::Arg(None) } @@ -6591,8 +6580,7 @@ impl<'db> TypeInferenceBuilder<'db> { value_ty: Type<'db>, ) -> Type<'db> { match value_ty { - Type::ClassLiteral(class_literal_ty) => match class_literal_ty.class().known(self.db()) - { + Type::ClassLiteral(class_literal) => match class_literal.known(self.db()) { Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice), Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice), _ => self.infer_subscript_type_expression(subscript, value_ty), @@ -6694,14 +6682,14 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Name(_) | ast::Expr::Attribute(_) => { let name_ty = self.infer_expression(slice); match name_ty { - Type::ClassLiteral(class_literal_ty) => { - if class_literal_ty - .class() - .is_known(self.db(), KnownClass::Any) - { + Type::ClassLiteral(class_literal) => { + if class_literal.is_known(self.db(), KnownClass::Any) { SubclassOfType::subclass_of_any() } else { - SubclassOfType::from(self.db(), class_literal_ty.class()) + SubclassOfType::from( + self.db(), + class_literal.default_specialization(self.db()), + ) } } Type::KnownInstance(KnownInstanceType::Any) => { @@ -6782,7 +6770,7 @@ impl<'db> TypeInferenceBuilder<'db> { } = subscript; match value_ty { - Type::ClassLiteral(literal) if literal.class().is_known(self.db(), KnownClass::Any) => { + Type::ClassLiteral(literal) if literal.is_known(self.db(), KnownClass::Any) => { self.context.report_lint( &INVALID_TYPE_FORM, subscript, diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index b87361e9db49f..aa2d9608006a5 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -4,34 +4,55 @@ use std::ops::Deref; use rustc_hash::FxHashSet; use crate::types::class_base::ClassBase; -use crate::types::{Class, Type}; +use crate::types::{ClassLiteralType, ClassType, Type}; use crate::Db; /// The inferred method resolution order of a given class. /// +/// An MRO cannot contain non-specialized generic classes. (This is why [`ClassBase`] contains a +/// [`ClassType`], not a [`ClassLiteralType`].) Any generic classes in a base class list are always +/// specialized — either because the class is explicitly specialized if there is a subscript +/// expression, or because we create the default specialization if there isn't. +/// +/// The MRO of a non-specialized generic class can contain generic classes that are specialized +/// with a typevar from the inheriting class. When the inheriting class is specialized, the MRO of +/// the resulting generic alias will substitute those type variables accordingly. For instance, in +/// the following example, the MRO of `D[int]` includes `C[int]`, and the MRO of `D[U]` includes +/// `C[U]` (which is a generic alias, not a non-specialized generic class): +/// +/// ```py +/// class C[T]: ... +/// class D[U](C[U]): ... +/// ``` +/// /// See [`Class::iter_mro`] for more details. #[derive(PartialEq, Eq, Clone, Debug, salsa::Update)] pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>); impl<'db> Mro<'db> { - /// Attempt to resolve the MRO of a given class + /// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of + /// base classes in the class definition, this operation is performed on a [class + /// literal][ClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a + /// class type, but this is done by first getting the MRO of the underlying class literal, and + /// specializing each base class as needed if the class type is a generic alias.) /// - /// In the event that a possible list of bases would (or could) lead to a - /// `TypeError` being raised at runtime due to an unresolvable MRO, we infer - /// the MRO of the class as being `[, Unknown, object]`. - /// This seems most likely to reduce the possibility of cascading errors - /// elsewhere. + /// In the event that a possible list of bases would (or could) lead to a `TypeError` being + /// raised at runtime due to an unresolvable MRO, we infer the MRO of the class as being `[, Unknown, object]`. This seems most likely to reduce the possibility of + /// cascading errors elsewhere. (For a generic class, the first entry in this fallback MRO uses + /// the default specialization of the class's type variables.) /// /// (We emit a diagnostic warning about the runtime `TypeError` in /// [`super::infer::TypeInferenceBuilder::infer_region_scope`].) - pub(super) fn of_class(db: &'db dyn Db, class: Class<'db>) -> Result> { - Self::of_class_impl(db, class).map_err(|error_kind| MroError { - kind: error_kind, - fallback_mro: Self::from_error(db, class), - }) + pub(super) fn of_class( + db: &'db dyn Db, + class: ClassLiteralType<'db>, + ) -> Result> { + Self::of_class_impl(db, class) + .map_err(|err| err.into_mro_error(db, class.default_specialization(db))) } - pub(super) fn from_error(db: &'db dyn Db, class: Class<'db>) -> Self { + pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { Self::from([ ClassBase::Class(class), ClassBase::unknown(), @@ -39,20 +60,26 @@ impl<'db> Mro<'db> { ]) } - fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result> { + fn of_class_impl( + db: &'db dyn Db, + class: ClassLiteralType<'db>, + ) -> Result> { let class_bases = class.explicit_bases(db); if !class_bases.is_empty() && class.inheritance_cycle(db).is_some() { // We emit errors for cyclically defined classes elsewhere. // It's important that we don't even try to infer the MRO for a cyclically defined class, // or we'll end up in an infinite loop. - return Ok(Mro::from_error(db, class)); + return Ok(Mro::from_error(db, class.default_specialization(db))); } match class_bases { // `builtins.object` is the special case: // the only class in Python that has an MRO with length <2 - [] if class.is_object(db) => Ok(Self::from([ClassBase::Class(class)])), + [] if class.is_object(db) => Ok(Self::from([ + // object is not generic, so the default specialization should be a no-op + ClassBase::Class(class.default_specialization(db)), + ])), // All other classes in Python have an MRO with length >=2. // Even if a class has no explicit base classes, @@ -67,7 +94,10 @@ impl<'db> Mro<'db> { // >>> Foo.__mro__ // (, ) // ``` - [] => Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)])), + [] => Ok(Self::from([ + ClassBase::Class(class.default_specialization(db)), + ClassBase::object(db), + ])), // Fast path for a class that has only a single explicit base. // @@ -77,9 +107,11 @@ impl<'db> Mro<'db> { [single_base] => ClassBase::try_from_type(db, *single_base).map_or_else( || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))), |single_base| { - Ok(std::iter::once(ClassBase::Class(class)) - .chain(single_base.mro(db)) - .collect()) + Ok( + std::iter::once(ClassBase::Class(class.default_specialization(db))) + .chain(single_base.mro(db)) + .collect(), + ) }, ), @@ -103,7 +135,9 @@ impl<'db> Mro<'db> { return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())); } - let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])]; + let mut seqs = vec![VecDeque::from([ClassBase::Class( + class.default_specialization(db), + )])]; for base in &valid_bases { seqs.push(base.mro(db).collect()); } @@ -118,7 +152,7 @@ impl<'db> Mro<'db> { .filter_map(|(index, base)| Some((index, base.into_class()?))) { if !seen_bases.insert(base) { - duplicate_bases.push((index, base)); + duplicate_bases.push((index, base.class_literal(db))); } } @@ -183,7 +217,7 @@ pub(super) struct MroIterator<'db> { db: &'db dyn Db, /// The class whose MRO we're iterating over - class: Class<'db>, + class: ClassLiteralType<'db>, /// Whether or not we've already yielded the first element of the MRO first_element_yielded: bool, @@ -197,7 +231,7 @@ pub(super) struct MroIterator<'db> { } impl<'db> MroIterator<'db> { - pub(super) fn new(db: &'db dyn Db, class: Class<'db>) -> Self { + pub(super) fn new(db: &'db dyn Db, class: ClassLiteralType<'db>) -> Self { Self { db, class, @@ -228,7 +262,7 @@ impl<'db> Iterator for MroIterator<'db> { fn next(&mut self) -> Option { if !self.first_element_yielded { self.first_element_yielded = true; - return Some(ClassBase::Class(self.class)); + return Some(ClassBase::Class(self.class.default_specialization(self.db))); } self.full_mro_except_first_element().next() } @@ -277,7 +311,7 @@ pub(super) enum MroErrorKind<'db> { /// of the duplicate bases. The indices are the indices of nodes /// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node. /// Each index is the index of a node representing a duplicate base. - DuplicateBases(Box<[(usize, Class<'db>)]>), + DuplicateBases(Box<[(usize, ClassLiteralType<'db>)]>), /// The MRO is otherwise unresolvable through the C3-merge algorithm. /// @@ -285,6 +319,15 @@ pub(super) enum MroErrorKind<'db> { UnresolvableMro { bases_list: Box<[ClassBase<'db>]> }, } +impl<'db> MroErrorKind<'db> { + pub(super) fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> MroError<'db> { + MroError { + kind: self, + fallback_mro: Mro::from_error(db, class), + } + } +} + /// Implementation of the [C3-merge algorithm] for calculating a Python class's /// [method resolution order]. /// diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 9410253a79a77..e920de404e00a 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -8,8 +8,8 @@ use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; use crate::semantic_index::symbol_table; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, SubclassOfType, - Truthiness, Type, UnionBuilder, + infer_expression_types, IntersectionBuilder, KnownClass, SubclassOfType, Truthiness, Type, + UnionBuilder, }; use crate::Db; use itertools::Itertools; @@ -116,10 +116,10 @@ impl KnownConstraintFunction { Type::ClassLiteral(class_literal) => { // At runtime (on Python 3.11+), this will return `True` for classes that actually // do inherit `typing.Any` and `False` otherwise. We could accurately model that? - if class_literal.class().is_known(db, KnownClass::Any) { + if class_literal.is_known(db, KnownClass::Any) { None } else { - Some(constraint_fn(class_literal.class())) + Some(constraint_fn(class_literal.default_specialization(db))) } } Type::SubclassOf(subclass_of_ty) => { @@ -428,7 +428,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { range: _, }, }) if keywords.is_empty() => { - let Type::ClassLiteral(ClassLiteralType { class: rhs_class }) = rhs_ty else { + let Type::ClassLiteral(rhs_class) = rhs_ty else { continue; }; @@ -451,10 +451,13 @@ impl<'db> NarrowingConstraintsBuilder<'db> { if callable_type .into_class_literal() - .is_some_and(|c| c.class().is_known(self.db, KnownClass::Type)) + .is_some_and(|c| c.is_known(self.db, KnownClass::Type)) { let symbol = self.expect_expr_name_symbol(id); - constraints.insert(symbol, Type::instance(rhs_class)); + constraints.insert( + symbol, + Type::instance(rhs_class.default_specialization(self.db)), + ); } } _ => {} @@ -505,7 +508,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { Type::ClassLiteral(class_type) if expr_call.arguments.args.len() == 1 && expr_call.arguments.keywords.is_empty() - && class_type.class().is_known(self.db, KnownClass::Bool) => + && class_type.is_known(self.db, KnownClass::Bool) => { self.evaluate_expression_node_predicate( &expr_call.arguments.args[0], diff --git a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs index 4dcc6b6c0812a..1fe881fbb385f 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs @@ -167,7 +167,7 @@ impl Ty { .symbol .expect_type() .expect_class_literal() - .class, + .default_specialization(db), ), Ty::SubclassOfAbcClass(s) => SubclassOfType::from( db, @@ -175,7 +175,7 @@ impl Ty { .symbol .expect_type() .expect_class_literal() - .class, + .default_specialization(db), ), Ty::AlwaysTruthy => Type::AlwaysTruthy, Ty::AlwaysFalsy => Type::AlwaysFalsy, diff --git a/crates/red_knot_python_semantic/src/types/slots.rs b/crates/red_knot_python_semantic/src/types/slots.rs index b0afda81baca6..19fac45d2463d 100644 --- a/crates/red_knot_python_semantic/src/types/slots.rs +++ b/crates/red_knot_python_semantic/src/types/slots.rs @@ -4,7 +4,7 @@ use crate::db::Db; use crate::symbol::{Boundness, Symbol}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::report_base_with_incompatible_slots; -use crate::types::{Class, ClassLiteralType, Type}; +use crate::types::{ClassLiteralType, Type}; use super::InferContext; @@ -23,7 +23,7 @@ enum SlotsKind { } impl SlotsKind { - fn from(db: &dyn Db, base: Class) -> Self { + fn from(db: &dyn Db, base: ClassLiteralType) -> Self { let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__").symbol else { return Self::NotSpecified; }; @@ -50,7 +50,11 @@ impl SlotsKind { } } -pub(super) fn check_class_slots(context: &InferContext, class: Class, node: &ast::StmtClassDef) { +pub(super) fn check_class_slots( + context: &InferContext, + class: ClassLiteralType, + node: &ast::StmtClassDef, +) { let db = context.db(); let mut first_with_solid_base = None; @@ -58,7 +62,7 @@ pub(super) fn check_class_slots(context: &InferContext, class: Class, node: &ast let mut found_second = false; for (index, base) in class.explicit_bases(db).iter().enumerate() { - let Type::ClassLiteral(ClassLiteralType { class: base }) = base else { + let Type::ClassLiteral(base) = base else { continue; }; @@ -67,7 +71,7 @@ pub(super) fn check_class_slots(context: &InferContext, class: Class, node: &ast return None; }; - match SlotsKind::from(db, current) { + match SlotsKind::from(db, current.class_literal(db)) { SlotsKind::NotEmpty => Some(current), SlotsKind::NotSpecified | SlotsKind::Empty => None, SlotsKind::Dynamic => None, diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index ab36d9ff36357..5526278b1ac51 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -1,6 +1,6 @@ use crate::symbol::SymbolAndQualifiers; -use super::{ClassBase, ClassLiteralType, Db, KnownClass, Type}; +use super::{ClassBase, Db, KnownClass, Type}; /// 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)] @@ -27,7 +27,7 @@ impl<'db> SubclassOfType<'db> { ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), ClassBase::Class(class) => { if class.is_final(db) { - Type::ClassLiteral(ClassLiteralType { class }) + Type::from(class) } else if class.is_object(db) { KnownClass::Type.to_instance(db) } else { diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 8b345bef28c0b..d0b6bc2beda5b 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -2,10 +2,7 @@ use std::cmp::Ordering; use crate::db::Db; -use super::{ - class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType, - TodoType, Type, -}; +use super::{class_base::ClassBase, DynamicType, InstanceType, KnownInstanceType, TodoType, Type}; /// Return an [`Ordering`] that describes the canonical order in which two types should appear /// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them @@ -97,10 +94,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::ModuleLiteral(_), _) => Ordering::Less, (_, Type::ModuleLiteral(_)) => Ordering::Greater, - ( - Type::ClassLiteral(ClassLiteralType { class: left }), - Type::ClassLiteral(ClassLiteralType { class: right }), - ) => left.cmp(right), + (Type::ClassLiteral(left), Type::ClassLiteral(right)) => left.cmp(right), (Type::ClassLiteral(_), _) => Ordering::Less, (_, Type::ClassLiteral(_)) => Ordering::Greater, From cc3a3dfeeb3ef0203f71f36afb6393c1eef2a53d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 4 Apr 2025 12:18:30 -0400 Subject: [PATCH 045/100] Add GenericAlias --- crates/red_knot_ide/src/lib.rs | 16 +++- crates/red_knot_python_semantic/src/types.rs | 90 +++++++++++++++---- .../src/types/class.rs | 40 ++++----- .../src/types/class_base.rs | 1 + .../src/types/display.rs | 19 ++++ .../src/types/generics.rs | 7 +- .../src/types/infer.rs | 5 +- .../src/types/type_ordering.rs | 4 + 8 files changed, 139 insertions(+), 43 deletions(-) diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index 7883388332c5d..eb18015000157 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -7,8 +7,8 @@ use std::ops::{Deref, DerefMut}; pub use db::Db; pub use goto::goto_type_definition; use red_knot_python_semantic::types::{ - ClassBase, ClassLiteralType, ClassType, FunctionType, InstanceType, IntersectionType, - KnownInstanceType, ModuleLiteralType, Type, + ClassBase, ClassLiteralType, ClassType, FunctionType, GenericAlias, InstanceType, + IntersectionType, KnownInstanceType, ModuleLiteralType, Type, }; use ruff_db::files::{File, FileRange}; use ruff_db::source::source_text; @@ -147,6 +147,7 @@ impl HasNavigationTargets for Type<'_> { .flat_map(|target| target.navigation_targets(db)) .collect(), Type::ClassLiteral(class) => class.navigation_targets(db), + Type::GenericAlias(alias) => alias.navigation_targets(db), Type::Instance(instance) => instance.navigation_targets(db), Type::KnownInstance(instance) => instance.navigation_targets(db), Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { @@ -209,6 +210,17 @@ impl HasNavigationTargets for ClassLiteralType<'_> { } } +impl HasNavigationTargets for GenericAlias<'_> { + fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + let class_range = self.focus_range(db.upcast()); + NavigationTargets::single(NavigationTarget { + file: class_range.file(), + focus_range: class_range.range(), + full_range: self.full_range(db.upcast()).range(), + }) + } +} + impl HasNavigationTargets for ClassType<'_> { fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { let class_range = self.focus_range(db.upcast()); diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e2bfe3ac9dbbb..1b20a3db8d9f7 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -42,7 +42,7 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; use crate::{Db, FxOrderSet, Module, Program}; pub(crate) use class::{Class, GenericClass, KnownClass, NonGenericClass}; -pub use class::{ClassLiteralType, ClassType, InstanceType, KnownInstanceType}; +pub use class::{ClassLiteralType, ClassType, GenericAlias, InstanceType, KnownInstanceType}; mod builder; mod call; @@ -284,7 +284,9 @@ pub enum Type<'db> { ModuleLiteral(ModuleLiteralType<'db>), /// A specific class object ClassLiteral(ClassLiteralType<'db>), - // The set of all class objects that are subclasses of the given class (C), spelled `type[C]`. + /// A specialization of a generic class + GenericAlias(GenericAlias<'db>), + /// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`. SubclassOf(SubclassOfType<'db>), /// The set of Python objects with the given class in their __class__'s method resolution order Instance(InstanceType<'db>), @@ -388,6 +390,12 @@ impl<'db> Type<'db> { | Self::WrapperDescriptor(_) | Self::MethodWrapper(_) => false, + Self::GenericAlias(generic) => generic + .specialization(db) + .types(db) + .iter() + .any(|ty| ty.contains_todo(db)), + Self::SpecializedCallable(specialized) => { specialized.callable_type(db).contains_todo(db) } @@ -671,6 +679,10 @@ impl<'db> Type<'db> { | Type::KnownInstance(_) | Type::IntLiteral(_) | Type::SubclassOf(_) => self, + Type::GenericAlias(generic) => { + let specialization = generic.specialization(db).normalized(db); + Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) + } Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Type::TypeVar(TypeVarInstance::new( @@ -918,6 +930,12 @@ impl<'db> Type<'db> { .subclass_of() .into_class() .is_some_and(|target_class| class.is_subclass_of(db, target_class)), + (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty + .subclass_of() + .into_class() + .is_some_and(|target_class| { + ClassType::from(alias).is_subclass_of(db, target_class) + }), // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { @@ -930,6 +948,9 @@ impl<'db> Type<'db> { (Type::ClassLiteral(class), _) => { class.metaclass_instance_type(db).is_subtype_of(db, target) } + (Type::GenericAlias(alias), _) => ClassType::from(alias) + .metaclass_instance_type(db) + .is_subtype_of(db, target), // `type[str]` (== `SubclassOf("str")` in red-knot) describes all possible runtime subclasses // of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str` @@ -1116,11 +1137,10 @@ impl<'db> Type<'db> { // Every class literal type is also assignable to `type[Any]`, because the class // literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable // to `type[Any]`. - (Type::ClassLiteral(_) | Type::SubclassOf(_), Type::SubclassOf(target_subclass_of)) - if target_subclass_of.is_dynamic() => - { - true - } + ( + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_), + Type::SubclassOf(target_subclass_of), + ) if target_subclass_of.is_dynamic() => true, // `type[Any]` is assignable to any type that `type[object]` is assignable to, because // `type[Any]` can materialize to `type[object]`. @@ -1361,6 +1381,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) + | Type::GenericAlias(..) | Type::KnownInstance(..)), right @ (Type::BooleanLiteral(..) | Type::IntLiteral(..) @@ -1373,6 +1394,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) + | Type::GenericAlias(..) | Type::KnownInstance(..)), ) => left != right, @@ -1381,6 +1403,7 @@ impl<'db> Type<'db> { ( Type::Tuple(..), Type::ClassLiteral(..) + | Type::GenericAlias(..) | Type::ModuleLiteral(..) | Type::BooleanLiteral(..) | Type::BytesLiteral(..) @@ -1395,6 +1418,7 @@ impl<'db> Type<'db> { ) | ( Type::ClassLiteral(..) + | Type::GenericAlias(..) | Type::ModuleLiteral(..) | Type::BooleanLiteral(..) | Type::BytesLiteral(..) @@ -1417,6 +1441,16 @@ impl<'db> Type<'db> { } } + (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) + | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { + match subclass_of_ty.subclass_of() { + ClassBase::Dynamic(_) => false, + ClassBase::Class(class_a) => { + !ClassType::from(alias_b).is_subclass_of(db, class_a) + } + } + } + ( Type::SubclassOf(_), Type::BooleanLiteral(..) @@ -1536,6 +1570,10 @@ impl<'db> Type<'db> { | (instance @ Type::Instance(_), Type::ClassLiteral(class)) => !class .metaclass_instance_type(db) .is_subtype_of(db, instance), + (Type::GenericAlias(alias), instance @ Type::Instance(_)) + | (instance @ Type::Instance(_), Type::GenericAlias(alias)) => !ClassType::from(alias) + .metaclass_instance_type(db) + .is_subtype_of(db, instance), (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => { @@ -1672,7 +1710,7 @@ impl<'db> Type<'db> { }, Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(), - Type::ClassLiteral(_) | Type::Instance(_) => { + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => { // TODO: Ideally, we would iterate over the MRO of the class, check if all // bases are fully static, and only return `true` if that is the case. // @@ -1746,6 +1784,7 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(..) | Type::WrapperDescriptor(..) | Type::ClassLiteral(..) + | Type::GenericAlias(..) | Type::ModuleLiteral(..) | Type::KnownInstance(..) => true, Type::Callable(_) => { @@ -1814,6 +1853,7 @@ impl<'db> Type<'db> { | Type::MethodWrapper(_) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) + | Type::GenericAlias(..) | Type::IntLiteral(..) | Type::BooleanLiteral(..) | Type::StringLiteral(..) @@ -1941,6 +1981,8 @@ impl<'db> Type<'db> { } } + Type::GenericAlias(alias) => Some(ClassType::from(*alias).class_member(db, name)), + Type::SubclassOf(subclass_of) if name == "__get__" && matches!( @@ -2103,7 +2145,9 @@ impl<'db> Type<'db> { // a `__dict__` that is filled with class level attributes. Modeling this is currently not // required, as `instance_member` is only called for instance-like types through `member`, // but we might want to add this in the future. - Type::ClassLiteral(_) | Type::SubclassOf(_) => Symbol::Unbound.into(), + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { + Symbol::Unbound.into() + } } } @@ -2455,12 +2499,10 @@ impl<'db> Type<'db> { }) } }, - Type::SpecializedCallable(specialized) => { - // XXX: specialize the result - specialized - .callable_type(db) - .member_lookup_with_policy(db, name, policy) - } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .member_lookup_with_policy(db, name, policy) + .map_type(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::MethodWrapper(_) => KnownClass::MethodWrapperType .to_instance(db) .member(db, &name), @@ -2573,7 +2615,7 @@ impl<'db> Type<'db> { } } - Type::ClassLiteral(..) | Type::SubclassOf(..) => { + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { let class_attr_plain = self.find_name_in_mro(db, name_str).expect( "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", ); @@ -2765,6 +2807,9 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) => class .metaclass_instance_type(db) .try_bool_impl(db, allow_short_circuit)?, + Type::GenericAlias(alias) => ClassType::from(*alias) + .metaclass_instance_type(db) + .try_bool_impl(db, allow_short_circuit)?, Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { ClassBase::Dynamic(_) => Truthiness::Ambiguous, @@ -3525,6 +3570,7 @@ impl<'db> Type<'db> { match self { Type::Dynamic(_) | Type::Never => Some(*self), Type::ClassLiteral(class) => Some(Type::instance(class.default_specialization(db))), + Type::GenericAlias(alias) => Some(Type::instance(ClassType::from(*alias))), Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()), Type::Union(union) => { let mut builder = UnionBuilder::new(db); @@ -3595,6 +3641,7 @@ impl<'db> Type<'db> { }; Ok(ty) } + Type::GenericAlias(alias) => Ok(Type::instance(ClassType::from(*alias))), Type::SubclassOf(_) | Type::BooleanLiteral(_) @@ -3842,6 +3889,7 @@ impl<'db> Type<'db> { }, Type::ClassLiteral(class) => class.metaclass(db), + Type::GenericAlias(alias) => ClassType::from(*alias).metaclass(db), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { ClassBase::Dynamic(_) => *self, ClassBase::Class(class) => SubclassOfType::from( @@ -3891,6 +3939,13 @@ impl<'db> Type<'db> { Type::SpecializedCallable(SpecializedCallableType::new(db, self, specialization)) } + Type::GenericAlias(generic) => { + let specialization = generic + .specialization(db) + .apply_specialization(db, specialization); + Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) + } + Type::Union(union) => union.map(db, |element| { element.apply_specialization(db, specialization) }), @@ -5079,7 +5134,8 @@ pub struct SpecializedCallableType<'db> { impl<'db> SpecializedCallableType<'db> { fn normalized(self, db: &'db dyn Db) -> Self { let callable_type = self.callable_type(db).normalized(db); - SpecializedCallableType::new(db, callable_type, self.specialization(db)) + let specialization = self.specialization(db).normalized(db); + SpecializedCallableType::new(db, callable_type, specialization) } } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index b8001ce365409..97450cbe820b9 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -140,27 +140,24 @@ pub struct GenericAlias<'db> { pub(crate) specialization: Specialization<'db>, } -impl<'db> From> for Type<'db> { - fn from(_alias: GenericAlias<'db>) -> Type<'db> { - // XXX: Type::GenericAlias(alias) - unreachable!() +impl<'db> GenericAlias<'db> { + /// Returns the file range of the class's name. + pub fn focus_range(self, db: &dyn Db) -> FileRange { + let class = self.origin(db).class(db); + FileRange::new(class.file(db), class.node(db).name.range) } -} -/* -impl<'db> GenericAlias<'db> { - /// Return an iterator over the inferred types of this generic alias's *explicit* bases, with - /// the alias's specialization applied. - pub(super) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { - self.explicit_bases_query(db) + pub fn full_range(self, db: &dyn Db) -> FileRange { + let class = self.origin(db).class(db); + FileRange::new(class.file(db), class.node(db).range) } +} - fn explicit_bases_query(self, db: &'db dyn Db) -> &'db [Type<'db>] { - // XXX: specialize result - ClassLiteralType::Generic(self).explicit_bases_query(db) +impl<'db> From> for Type<'db> { + fn from(alias: GenericAlias<'db>) -> Type<'db> { + Type::GenericAlias(alias) } } -*/ /// Represents a class type, which might be a non-generic class, or a specialization of a generic /// class. @@ -293,13 +290,6 @@ impl<'db> ClassType<'db> { .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") } - /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. - pub(super) fn try_metaclass(self, db: &'db dyn Db) -> Result, MetaclassError<'db>> { - self.class_literal(db) - .try_metaclass(db) - .map(|ty| self.specialize_type(db, ty)) - } - /// Returns the class member of this class named `name`. /// /// The member resolves to a member on the class itself or any of its proper superclasses. @@ -523,6 +513,12 @@ impl<'db> ClassType<'db> { } } +impl<'db> From> for ClassType<'db> { + fn from(generic: GenericAlias<'db>) -> ClassType<'db> { + ClassType::Generic(generic) + } +} + impl<'db> From> for Type<'db> { fn from(class: ClassType<'db>) -> Type<'db> { match class { diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 779e95800d60d..7057b48b0299f 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -94,6 +94,7 @@ impl<'db> ClassBase<'db> { } else { Self::Class(literal.default_specialization(db)) }), + Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? Type::Instance(_) => None, // TODO -- handle `__mro_entries__`? diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 8a1af518d4726..0e6c0f7ddd0d6 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -83,6 +83,25 @@ impl Display for DisplayRepresentation<'_> { } // TODO functions and classes should display using a fully qualified name Type::ClassLiteral(class) => f.write_str(class.name(self.db)), + Type::GenericAlias(generic) => { + write!( + f, + "{origin}[", + origin = generic.origin(self.db).class(self.db).name, + )?; + for (idx, ty) in generic + .specialization(self.db) + .types(self.db) + .iter() + .enumerate() + { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{}", ty.display(self.db))?; + } + f.write_str("]") + } Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { // Only show the bare class name here; ClassBase::display would render this as // type[] instead of type[Foo]. diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 87f52f28e0a0f..a10406b3be531 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -97,7 +97,7 @@ impl<'db> GenericContext<'db> { #[salsa::tracked(debug)] pub struct Specialization<'db> { generic_context: GenericContext<'db>, - types: Box<[Type<'db>]>, + pub(crate) types: Box<[Type<'db>]>, } impl<'db> Specialization<'db> { @@ -123,6 +123,11 @@ impl<'db> Specialization<'db> { Specialization::new(db, self.generic_context(db), types) } + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + let types = self.types(db).iter().map(|ty| ty.normalized(db)).collect(); + 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> { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index b6d19e3bb7524..cc01a79540a95 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2483,7 +2483,7 @@ impl<'db> TypeInferenceBuilder<'db> { } }, - Type::ClassLiteral(..) | Type::SubclassOf(..) => { + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { match object_ty.class_member(db, attribute.into()) { SymbolAndQualifiers { symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), @@ -4475,6 +4475,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::SpecializedCallable(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) + | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::Instance(_) | Type::KnownInstance(_) @@ -4752,6 +4753,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::MethodWrapper(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) + | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::Instance(_) | Type::KnownInstance(_) @@ -4774,6 +4776,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::MethodWrapper(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) + | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::Instance(_) | Type::KnownInstance(_) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index d0b6bc2beda5b..58dfb91245ff6 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -98,6 +98,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::ClassLiteral(_), _) => Ordering::Less, (_, Type::ClassLiteral(_)) => Ordering::Greater, + (Type::GenericAlias(left), Type::GenericAlias(right)) => left.cmp(right), + (Type::GenericAlias(_), _) => Ordering::Less, + (_, Type::GenericAlias(_)) => Ordering::Greater, + (Type::SubclassOf(left), Type::SubclassOf(right)) => { match (left.subclass_of(), right.subclass_of()) { (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), From 18af8b6f94a5505f6b62c794f2ef2ac006574b64 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 4 Apr 2025 12:22:15 -0400 Subject: [PATCH 046/100] Apply specializations --- crates/red_knot_python_semantic/src/types.rs | 50 ++++++++++--------- .../src/types/class_base.rs | 2 +- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1b20a3db8d9f7..72ec1647d2b54 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -463,7 +463,7 @@ impl<'db> Type<'db> { Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { Some(ClassType::NonGeneric(non_generic)) } - // XXX: GenericAlias + Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), _ => None, } } @@ -475,8 +475,10 @@ impl<'db> Type<'db> { } pub const fn is_class_type(&self) -> bool { - // XXX: GenericAlias - matches!(self, Type::ClassLiteral(ClassLiteralType::NonGeneric(_))) + matches!( + self, + Type::ClassLiteral(ClassLiteralType::NonGeneric(_)) | Type::GenericAlias(_) + ) } pub const fn is_instance(&self) -> bool { @@ -2013,10 +2015,12 @@ impl<'db> Type<'db> { .find_name_in_mro(db, name) } - Type::SpecializedCallable(specialized) => { - // XXX: specialize the result - specialized.callable_type(db).find_name_in_mro(db, name) - } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .find_name_in_mro(db, name) + .map(|sq| { + sq.map_type(|ty| ty.apply_specialization(db, specialized.specialization(db))) + }), Type::FunctionLiteral(_) | Type::Callable(_) @@ -2098,10 +2102,10 @@ impl<'db> Type<'db> { Type::BoundMethod(_) => KnownClass::MethodType .to_instance(db) .instance_member(db, name), - Type::SpecializedCallable(specialized) => { - // XXX: specialize the result - specialized.callable_type(db).instance_member(db, name) - } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .instance_member(db, name) + .map_type(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::MethodWrapper(_) => KnownClass::MethodWrapperType .to_instance(db) .instance_member(db, name), @@ -3580,10 +3584,10 @@ impl<'db> Type<'db> { Some(builder.build()) } Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance()")), - Type::SpecializedCallable(specialized) => { - // XXX: specialize the result - specialized.callable_type(db).to_instance(db) - } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .to_instance(db) + .map(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::FunctionLiteral(_) @@ -3666,10 +3670,10 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }), - Type::SpecializedCallable(specialized) => { - // XXX: specialize the result - specialized.callable_type(db).in_type_expression(db) - } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .in_type_expression(db) + .map(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), @@ -3868,10 +3872,10 @@ impl<'db> Type<'db> { Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::SpecializedCallable(specialized) => { - // XXX: specialize the result - specialized.callable_type(db).to_meta_type(db) - } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .to_meta_type(db) + .apply_specialization(db, specialized.specialization(db)), Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), Type::Callable(_) => KnownClass::Type.to_instance(db), diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 7057b48b0299f..93dffc2294698 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -118,8 +118,8 @@ impl<'db> ClassBase<'db> { | Type::AlwaysFalsy | Type::AlwaysTruthy => None, Type::SpecializedCallable(specialized) => { - // XXX: Specialize the result Self::try_from_type(db, specialized.callable_type(db)) + .map(|ty| ty.apply_specialization(db, specialized.specialization(db))) } Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeVar(_) From af52fd16eaa98b5354a042bf68ad43a21a84f02c Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 4 Apr 2025 14:15:28 -0400 Subject: [PATCH 047/100] Use class literal for self member lookups --- .../src/types/class.rs | 400 +++++++++--------- 1 file changed, 210 insertions(+), 190 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 97450cbe820b9..94b86b5d4f685 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -186,10 +186,6 @@ impl<'db> ClassType<'db> { self.class(db).known } - pub(crate) fn body_scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.class(db).body_scope - } - /// Returns the file range of the class's name. pub fn focus_range(self, db: &dyn Db) -> FileRange { let class = self.class(db); @@ -325,191 +321,12 @@ impl<'db> ClassType<'db> { .map_type(|ty| self.specialize_type(db, ty)) } - /// Tries to find declarations/bindings of an instance attribute named `name` that are only - /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. - fn implicit_instance_attribute( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: &str, - ) -> Option> { - // If we do not see any declarations of an attribute, neither in the class body nor in - // any method, we build a union of `Unknown` with the inferred types of all bindings of - // that attribute. We include `Unknown` in that union to account for the fact that the - // attribute might be externally modified. - let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); - - let attribute_assignments = attribute_assignments(db, class_body_scope); - - let attribute_assignments = attribute_assignments - .as_deref() - .and_then(|assignments| assignments.get(name))?; - - for attribute_assignment in attribute_assignments { - match attribute_assignment { - AttributeAssignment::Annotated { annotation } => { - // We found an annotated assignment of one of the following forms (using 'self' in these - // examples, but we support arbitrary names for the first parameters of methods): - // - // self.name: - // self.name: = … - - let annotation_ty = infer_expression_type(db, *annotation); - - // TODO: check if there are conflicting declarations - return Some(annotation_ty); - } - AttributeAssignment::Unannotated { value } => { - // We found an un-annotated attribute assignment of the form: - // - // self.name = - - let inferred_ty = infer_expression_type(db, *value); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - AttributeAssignment::Iterable { iterable } => { - // We found an attribute assignment like: - // - // for self.name in : - - let iterable_ty = infer_expression_type(db, *iterable); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = iterable_ty.iterate(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - AttributeAssignment::ContextManager { context_manager } => { - // We found an attribute assignment like: - // - // with as self.name: - - let context_ty = infer_expression_type(db, *context_manager); - let inferred_ty = context_ty.enter(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - AttributeAssignment::Unpack { - attribute_expression_id, - unpack, - } => { - // We found an unpacking assignment like: - // - // .., self.name, .. = - // (.., self.name, ..) = - // [.., self.name, ..] = - - let inferred_ty = - infer_unpack_types(db, *unpack).expression_type(*attribute_expression_id); - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - - Some(union_of_inferred_types.build()) - } - /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - // TODO: There are many things that are not yet implemented here: - // - `typing.Final` - // - Proper diagnostics - - let body_scope = self.body_scope(db); - let table = symbol_table(db, body_scope); - - if let Some(symbol_id) = table.symbol_id_by_name(name) { - let use_def = use_def_map(db, body_scope); - - let declarations = use_def.public_declarations(symbol_id); - let declared_and_qualifiers = symbol_from_declarations(db, declarations); - match declared_and_qualifiers { - Ok(SymbolAndQualifiers { - symbol: declared @ Symbol::Type(declared_ty, declaredness), - qualifiers, - }) => { - // The attribute is declared in the class body. - - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); - let has_binding = !inferred.is_unbound(); - - if has_binding { - // The attribute is declared and bound in the class body. - - if let Some(implicit_ty) = - Self::implicit_instance_attribute(db, body_scope, name) - { - if declaredness == Boundness::Bound { - // If a symbol is definitely declared, and we see - // attribute assignments in methods of the class, - // we trust the declared type. - declared.with_qualifiers(qualifiers) - } else { - Symbol::Type( - UnionType::from_elements(db, [declared_ty, implicit_ty]), - declaredness, - ) - .with_qualifiers(qualifiers) - } - } else { - // The symbol is declared and bound in the class body, - // but we did not find any attribute assignments in - // methods of the class. This means that the attribute - // has a class-level default value, but it would not be - // found in a `__dict__` lookup. - - Symbol::Unbound.into() - } - } else { - // The attribute is declared but not bound in the class body. - // We take this as a sign that this is intended to be a pure - // instance attribute, and we trust the declared type, unless - // it is possibly-undeclared. In the latter case, we also - // union with the inferred type from attribute assignments. - - if declaredness == Boundness::Bound { - declared.with_qualifiers(qualifiers) - } else { - if let Some(implicit_ty) = - Self::implicit_instance_attribute(db, body_scope, name) - { - Symbol::Type( - UnionType::from_elements(db, [declared_ty, implicit_ty]), - declaredness, - ) - .with_qualifiers(qualifiers) - } else { - declared.with_qualifiers(qualifiers) - } - } - } - } - - Ok(SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers: _, - }) => { - // The attribute is not *declared* in the class body. It could still be declared/bound - // in a method. - - Self::implicit_instance_attribute(db, body_scope, name) - .map_or(Symbol::Unbound, Symbol::bound) - .into() - } - Err((declared, _conflicting_declarations)) => { - // There are conflicting declarations for this attribute in the class body. - Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) - } - } - } else { - // This attribute is neither declared nor bound in the class body. - // It could still be implicitly defined in a method. - - Self::implicit_instance_attribute(db, body_scope, name) - .map_or(Symbol::Unbound, Symbol::bound) - .into() - } + self.class_literal(db) + .own_instance_member(db, name) + .map_type(|ty| self.specialize_type(db, ty)) } } @@ -855,7 +672,7 @@ impl<'db> ClassLiteralType<'db> { let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound(TypeQualifiers::empty())); - for superclass in self.iter_mro(db) { + for (idx, superclass) in self.iter_mro(db).enumerate() { match superclass { ClassBase::Dynamic(DynamicType::TodoProtocol) => { // TODO: We currently skip `Protocol` when looking up class members, in order to @@ -870,7 +687,15 @@ impl<'db> ClassLiteralType<'db> { } ClassBase::Class(class) => { lookup_result = lookup_result.or_else(|lookup_error| { - lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) + let member = if idx == 0 { + // The first element of the MRO is always the class itself. For that + // class, we want to look up the member in the class literal, so that + // we don't apply the default specialization. + self.own_class_member(db, name) + } else { + class.own_class_member(db, name) + }; + lookup_error.or_fall_back_to(db, member) }); } } @@ -930,7 +755,7 @@ impl<'db> ClassLiteralType<'db> { let mut union = UnionBuilder::new(db); let mut union_qualifiers = TypeQualifiers::empty(); - for superclass in self.iter_mro(db) { + for (idx, superclass) in self.iter_mro(db).enumerate() { match superclass { ClassBase::Dynamic(DynamicType::TodoProtocol) => { // TODO: We currently skip `Protocol` when looking up instance members, in order to @@ -943,10 +768,18 @@ impl<'db> ClassLiteralType<'db> { ); } ClassBase::Class(class) => { + let member = if idx == 0 { + // The first element of the MRO is always the class itself. For that + // class, we want to look up the member in the class literal, so that + // we don't apply the default specialization. + self.own_instance_member(db, name) + } else { + class.own_instance_member(db, name) + }; if let member @ SymbolAndQualifiers { symbol: Symbol::Type(ty, boundness), qualifiers, - } = class.own_instance_member(db, name) + } = member { // TODO: We could raise a diagnostic here if there are conflicting type qualifiers union_qualifiers |= qualifiers; @@ -980,6 +813,193 @@ impl<'db> ClassLiteralType<'db> { } } + /// Tries to find declarations/bindings of an instance attribute named `name` that are only + /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. + fn implicit_instance_attribute( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &str, + ) -> Option> { + // If we do not see any declarations of an attribute, neither in the class body nor in + // any method, we build a union of `Unknown` with the inferred types of all bindings of + // that attribute. We include `Unknown` in that union to account for the fact that the + // attribute might be externally modified. + let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); + + let attribute_assignments = attribute_assignments(db, class_body_scope); + + let attribute_assignments = attribute_assignments + .as_deref() + .and_then(|assignments| assignments.get(name))?; + + for attribute_assignment in attribute_assignments { + match attribute_assignment { + AttributeAssignment::Annotated { annotation } => { + // We found an annotated assignment of one of the following forms (using 'self' in these + // examples, but we support arbitrary names for the first parameters of methods): + // + // self.name: + // self.name: = … + + let annotation_ty = infer_expression_type(db, *annotation); + + // TODO: check if there are conflicting declarations + return Some(annotation_ty); + } + AttributeAssignment::Unannotated { value } => { + // We found an un-annotated attribute assignment of the form: + // + // self.name = + + let inferred_ty = infer_expression_type(db, *value); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + AttributeAssignment::Iterable { iterable } => { + // We found an attribute assignment like: + // + // for self.name in : + + let iterable_ty = infer_expression_type(db, *iterable); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = iterable_ty.iterate(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + AttributeAssignment::ContextManager { context_manager } => { + // We found an attribute assignment like: + // + // with as self.name: + + let context_ty = infer_expression_type(db, *context_manager); + let inferred_ty = context_ty.enter(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + AttributeAssignment::Unpack { + attribute_expression_id, + unpack, + } => { + // We found an unpacking assignment like: + // + // .., self.name, .. = + // (.., self.name, ..) = + // [.., self.name, ..] = + + let inferred_ty = + infer_unpack_types(db, *unpack).expression_type(*attribute_expression_id); + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + + Some(union_of_inferred_types.build()) + } + + /// A helper function for `instance_member` that looks up the `name` attribute only on + /// this class, not on its superclasses. + fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + // TODO: There are many things that are not yet implemented here: + // - `typing.Final` + // - Proper diagnostics + + let body_scope = self.body_scope(db); + let table = symbol_table(db, body_scope); + + if let Some(symbol_id) = table.symbol_id_by_name(name) { + let use_def = use_def_map(db, body_scope); + + let declarations = use_def.public_declarations(symbol_id); + let declared_and_qualifiers = symbol_from_declarations(db, declarations); + match declared_and_qualifiers { + Ok(SymbolAndQualifiers { + symbol: declared @ Symbol::Type(declared_ty, declaredness), + qualifiers, + }) => { + // The attribute is declared in the class body. + + let bindings = use_def.public_bindings(symbol_id); + let inferred = symbol_from_bindings(db, bindings); + let has_binding = !inferred.is_unbound(); + + if has_binding { + // The attribute is declared and bound in the class body. + + if let Some(implicit_ty) = + Self::implicit_instance_attribute(db, body_scope, name) + { + if declaredness == Boundness::Bound { + // If a symbol is definitely declared, and we see + // attribute assignments in methods of the class, + // we trust the declared type. + declared.with_qualifiers(qualifiers) + } else { + Symbol::Type( + UnionType::from_elements(db, [declared_ty, implicit_ty]), + declaredness, + ) + .with_qualifiers(qualifiers) + } + } else { + // The symbol is declared and bound in the class body, + // but we did not find any attribute assignments in + // methods of the class. This means that the attribute + // has a class-level default value, but it would not be + // found in a `__dict__` lookup. + + Symbol::Unbound.into() + } + } else { + // The attribute is declared but not bound in the class body. + // We take this as a sign that this is intended to be a pure + // instance attribute, and we trust the declared type, unless + // it is possibly-undeclared. In the latter case, we also + // union with the inferred type from attribute assignments. + + if declaredness == Boundness::Bound { + declared.with_qualifiers(qualifiers) + } else { + if let Some(implicit_ty) = + Self::implicit_instance_attribute(db, body_scope, name) + { + Symbol::Type( + UnionType::from_elements(db, [declared_ty, implicit_ty]), + declaredness, + ) + .with_qualifiers(qualifiers) + } else { + declared.with_qualifiers(qualifiers) + } + } + } + } + + Ok(SymbolAndQualifiers { + symbol: Symbol::Unbound, + qualifiers: _, + }) => { + // The attribute is not *declared* in the class body. It could still be declared/bound + // in a method. + + Self::implicit_instance_attribute(db, body_scope, name) + .map_or(Symbol::Unbound, Symbol::bound) + .into() + } + Err((declared, _conflicting_declarations)) => { + // There are conflicting declarations for this attribute in the class body. + Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) + } + } + } else { + // This attribute is neither declared nor bound in the class body. + // It could still be implicitly defined in a method. + + Self::implicit_instance_attribute(db, body_scope, name) + .map_or(Symbol::Unbound, Symbol::bound) + .into() + } + } + /// Return this class' involvement in an inheritance cycle, if any. /// /// A class definition like this will fail at runtime, From d3fd82268f003d981145683930efe9befcf7a20a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 4 Apr 2025 14:20:36 -0400 Subject: [PATCH 048/100] Display specializations and generic aliases --- .../resources/mdtest/generics/classes.md | 48 ++++------ .../resources/mdtest/generics/scoping.md | 4 - crates/red_knot_python_semantic/src/types.rs | 20 ++++ .../src/types/display.rs | 94 +++++++++++++----- .../src/types/generics.rs | 12 ++- .../src/types/infer.rs | 11 ++- .../src/types/signatures.rs | 96 +++++++++++++++++++ 7 files changed, 213 insertions(+), 72 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 98b78eb77b662..5dd01a0c2525f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -61,8 +61,7 @@ The type parameter can be specified explicitly: class C[T]: x: T -# TODO: revealed: C[int] -reveal_type(C[int]()) # revealed: C +reveal_type(C[int]()) # revealed: C[int] ``` The specialization must match the generic types: @@ -79,11 +78,8 @@ class Bounded[T: int]: ... class BoundedByUnion[T: int | str]: ... class IntSubclass(int): ... -# TODO: revealed: Bounded[int] -reveal_type(Bounded[int]()) # revealed: Bounded - -# TODO: revealed: Bounded[IntSubclass] -reveal_type(Bounded[IntSubclass]()) # revealed: Bounded +reveal_type(Bounded[int]()) # revealed: Bounded[int] +reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] # error: [invalid-argument-type] "Object of type `str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`" reveal_type(Bounded[str]()) # revealed: Unknown @@ -91,17 +87,10 @@ reveal_type(Bounded[str]()) # revealed: Unknown # error: [invalid-argument-type] "Object of type `int | str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`" reveal_type(Bounded[int | str]()) # revealed: Unknown -# TODO: revealed: BoundedByUnion[int] -reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion - -# TODO: revealed: BoundedByUnion[IntSubclass] -reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion - -# TODO: revealed: BoundedByUnion[str] -reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion - -# TODO: revealed: BoundedByUnion[int | str] -reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion +reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] +reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass] +reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] +reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] ``` If the type variable is constrained, the specialized type must satisfy those constraints: @@ -109,15 +98,13 @@ If the type variable is constrained, the specialized type must satisfy those con ```py class Constrained[T: (int, str)]: ... -# TODO: revealed: Constrained[int] -reveal_type(Constrained[int]()) # revealed: Constrained +reveal_type(Constrained[int]()) # revealed: Constrained[int] # TODO: error: [invalid-argument-type] # TODO: revealed: Unknown -reveal_type(Constrained[IntSubclass]()) # revealed: Constrained +reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] -# TODO: revealed: Constrained[str] -reveal_type(Constrained[str]()) # revealed: Constrained +reveal_type(Constrained[str]()) # revealed: Constrained[str] # error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`" reveal_type(Constrained[object]()) # revealed: Unknown @@ -133,14 +120,14 @@ class C[T]: c: C[int] = C() # TODO: revealed: C[int] -reveal_type(c) # revealed: C +reveal_type(c) # revealed: C[Unknown] ``` The typevars of a fully specialized generic class should no longer be visible: ```py # TODO: revealed: int -reveal_type(c.x) # revealed: T +reveal_type(c.x) # revealed: Unknown ``` If the type parameter is not specified explicitly, and there are no constraints that let us infer a @@ -149,15 +136,13 @@ specific type, we infer the typevar's default type: ```py class D[T = int]: ... -# TODO: revealed: D[int] -reveal_type(D()) # revealed: D +reveal_type(D()) # revealed: D[int] ``` If a typevar does not provide a default, we use `Unknown`: ```py -# TODO: revealed: C[Unknown] -reveal_type(C()) # revealed: C +reveal_type(C()) # revealed: C[Unknown] ``` If the type of a constructor parameter is a class typevar, we can use that to infer the type @@ -168,7 +153,7 @@ class E[T]: def __init__(self, x: T) -> None: ... # TODO: revealed: E[int] or E[Literal[1]] -reveal_type(E(1)) # revealed: E +reveal_type(E(1)) # revealed: E[Unknown] ``` The types inferred from a type context and from a constructor parameter must be consistent with each @@ -190,8 +175,7 @@ class Base[T]: class Sub[U](Base[U]): ... -# TODO: revealed: int | None -reveal_type(Base[int].x) # revealed: T | None +reveal_type(Base[int].x) # revealed: int | None # TODO: revealed: int | None reveal_type(Sub[int].x) # revealed: T | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 0d21a82294ee4..7cdf663a25e2f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -83,11 +83,7 @@ class C[T]: return x c: C[int] = C() -# TODO: no error -# error: [invalid-argument-type] c.m1(1) -# TODO: no error -# error: [invalid-argument-type] c.m2(1) # TODO: expected type `int` # error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `T`" diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 72ec1647d2b54..03a75982a1179 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2926,6 +2926,11 @@ impl<'db> Type<'db> { Signatures::single(signature) } + Type::SpecializedCallable(specialized) => specialized + .callable_type(db) + .signatures(db) + .apply_specialization(db, specialized.specialization(db)), + Type::MethodWrapper( MethodWrapperKind::FunctionTypeDunderGet(_) | MethodWrapperKind::PropertyDunderGet(_), @@ -3329,6 +3334,21 @@ impl<'db> Type<'db> { } }, + Type::GenericAlias(_) => { + // TODO annotated return type on `__new__` or metaclass `__call__` + // TODO check call vs signatures of `__new__` and/or `__init__` + eprintln!( + "==> instantiate generic alias {} to {}", + self.display(db), + self.to_instance(db).unwrap_or(Type::unknown()).display(db) + ); + let signature = CallableSignature::single( + self, + Signature::new(Parameters::gradual_form(), self.to_instance(db)), + ); + Signatures::single(signature) + } + Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db), ClassBase::Class(class) => Type::from(class).signatures(db), diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 0e6c0f7ddd0d6..cfb1ad7ac1a4c 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -6,11 +6,13 @@ use ruff_db::display::FormatterJoinExtension; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; +use crate::types::class::{ClassType, GenericAlias, GenericClass}; use crate::types::class_base::ClassBase; +use crate::types::generics::Specialization; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ InstanceType, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, Type, - UnionType, WrapperDescriptorKind, + TypeVarInstance, UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -69,14 +71,12 @@ impl Display for DisplayRepresentation<'_> { match self.ty { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), - Type::Instance(InstanceType { class }) => { - let representation = match class.known(self.db) { - Some(KnownClass::NoneType) => "None", - Some(KnownClass::NoDefaultType) => "NoDefault", - _ => class.name(self.db), - }; - f.write_str(representation) - } + Type::Instance(InstanceType { class }) => match (class, class.known(self.db)) { + (_, Some(KnownClass::NoneType)) => f.write_str("None"), + (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), + (ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name), + (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), + }, Type::PropertyInstance(_) => f.write_str("property"), Type::ModuleLiteral(module) => { write!(f, "", module.module(self.db).name()) @@ -84,23 +84,7 @@ impl Display for DisplayRepresentation<'_> { // TODO functions and classes should display using a fully qualified name Type::ClassLiteral(class) => f.write_str(class.name(self.db)), Type::GenericAlias(generic) => { - write!( - f, - "{origin}[", - origin = generic.origin(self.db).class(self.db).name, - )?; - for (idx, ty) in generic - .specialization(self.db) - .types(self.db) - .iter() - .enumerate() - { - if idx > 0 { - f.write_str(", ")?; - } - write!(f, "{}", ty.display(self.db))?; - } - f.write_str("]") + write!(f, "{}", generic.display(self.db)) } Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { // Only show the bare class name here; ClassBase::display would render this as @@ -200,6 +184,64 @@ impl Display for DisplayRepresentation<'_> { } } +impl<'db> GenericAlias<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { + DisplayGenericAlias { + origin: self.origin(db), + types: self.specialization(db).types(db), + db, + } + } +} + +pub(crate) struct DisplayGenericAlias<'db> { + origin: GenericClass<'db>, + types: &'db [Type<'db>], + db: &'db dyn Db, +} + +impl Display for DisplayGenericAlias<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{origin}[", origin = self.origin.class(self.db).name,)?; + for (idx, ty) in self.types.iter().enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{}", ty.display(self.db))?; + } + f.write_str("]") + } +} + +impl<'db> Specialization<'db> { + pub fn display(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + DisplaySpecialization { + typevars: self.generic_context(db).variables(db), + types: self.types(db), + db, + } + } +} + +pub struct DisplaySpecialization<'db> { + typevars: &'db [TypeVarInstance<'db>], + types: &'db [Type<'db>], + db: &'db dyn Db, +} + +impl Display for DisplaySpecialization<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char('{')?; + for (idx, (var, ty)) in self.typevars.into_iter().zip(self.types).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{} = {}", var.name(self.db), ty.display(self.db))?; + } + f.write_char('}') + } +} + impl<'db> Signature<'db> { fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> { DisplaySignature { diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index a10406b3be531..946b38f75b510 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -11,7 +11,8 @@ use crate::Db; /// A list of formal type variables for a generic function, class, or type alias. #[salsa::tracked(debug)] pub struct GenericContext<'db> { - variables: Box<[TypeVarInstance<'db>]>, + #[return_ref] + pub(crate) variables: Box<[TypeVarInstance<'db>]>, } impl<'db> GenericContext<'db> { @@ -96,7 +97,8 @@ impl<'db> GenericContext<'db> { /// An assignment of a specific type to each type variable in a generic scope. #[salsa::tracked(debug)] pub struct Specialization<'db> { - generic_context: GenericContext<'db>, + pub(crate) generic_context: GenericContext<'db>, + #[return_ref] pub(crate) types: Box<[Type<'db>]>, } @@ -117,7 +119,7 @@ impl<'db> Specialization<'db> { pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self { let types = self .types(db) - .iter() + .into_iter() .map(|ty| ty.apply_specialization(db, other)) .collect(); Specialization::new(db, self.generic_context(db), types) @@ -133,9 +135,9 @@ impl<'db> Specialization<'db> { pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option> { self.generic_context(db) .variables(db) - .iter() + .into_iter() .zip(self.types(db)) .find(|(var, _)| **var == typevar) - .map(|(_, ty)| ty) + .map(|(_, ty)| *ty) } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index cc01a79540a95..8e74dd4f6a5be 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -78,7 +78,7 @@ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - todo_type, Class, ClassLiteralType, DynamicType, FunctionType, GenericClass, + todo_type, Class, ClassLiteralType, DynamicType, FunctionType, GenericAlias, GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, NonGenericClass, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, @@ -5673,7 +5673,7 @@ impl<'db> TypeInferenceBuilder<'db> { return self.infer_explicit_class_specialization( subscript, value_ty, - generic_class.generic_context(self.db()), + generic_class, slice, ); } @@ -5686,7 +5686,7 @@ impl<'db> TypeInferenceBuilder<'db> { &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, - generic_context: GenericContext<'db>, + generic_class: GenericClass<'db>, slice_node: &ast::Expr, ) -> Type<'db> { let mut call_argument_types = match slice_node { @@ -5695,6 +5695,7 @@ impl<'db> TypeInferenceBuilder<'db> { ), _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), }; + let generic_context = generic_class.generic_context(self.db()); let signatures = Signatures::single(CallableSignature::single( value_ty, generic_context.signature(self.db()), @@ -5715,7 +5716,7 @@ impl<'db> TypeInferenceBuilder<'db> { let (_, overload) = callable .matching_overload() .expect("valid bindings should have matching overload"); - let _specialization = generic_context.specialize( + let specialization = generic_context.specialize( self.db(), overload .parameter_types() @@ -5723,7 +5724,7 @@ impl<'db> TypeInferenceBuilder<'db> { .map(|ty| ty.unwrap_or(Type::unknown())) .collect(), ); - value_ty + Type::from(GenericAlias::new(self.db(), generic_class, specialization)) } fn infer_subscript_expression_types( diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index f67fa7c79c4ce..ef9d7f90c15a0 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -14,6 +14,7 @@ use smallvec::{smallvec, SmallVec}; use super::{definition_expression_type, DynamicType, Type}; use crate::semantic_index::definition::Definition; +use crate::types::generics::Specialization; use crate::types::todo_type; use crate::Db; use ruff_python_ast::{self as ast, name::Name}; @@ -66,6 +67,25 @@ impl<'db> Signatures<'db> { } } + pub(crate) fn apply_specialization( + &self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + let callable_type = self.callable_type.apply_specialization(db, specialization); + let signature_type = self.signature_type.apply_specialization(db, specialization); + let elements = self + .elements + .iter() + .map(|callable| callable.apply_specialization(db, specialization)) + .collect(); + Self { + callable_type, + signature_type, + elements, + } + } + pub(crate) fn iter(&self) -> std::slice::Iter<'_, CallableSignature<'db>> { self.elements.iter() } @@ -183,6 +203,26 @@ impl<'db> CallableSignature<'db> { self } + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let callable_type = self.callable_type.apply_specialization(db, specialization); + let signature_type = self.signature_type.apply_specialization(db, specialization); + let bound_type = self + .bound_type + .map(|ty| ty.apply_specialization(db, specialization)); + let overloads = self + .overloads + .iter() + .map(|overload| overload.apply_specialization(db, specialization)) + .collect(); + Self { + callable_type, + signature_type, + dunder_call_is_possibly_unbound: self.dunder_call_is_possibly_unbound, + bound_type, + overloads, + } + } + pub(crate) fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> { self.overloads.iter() } @@ -261,6 +301,17 @@ impl<'db> Signature<'db> { } } + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let parameters = self.parameters.apply_specialization(db, specialization); + let return_ty = self + .return_ty + .map(|ty| ty.apply_specialization(db, specialization)); + Self { + parameters, + return_ty, + } + } + /// Return the parameters in this signature. pub(crate) fn parameters(&self) -> &Parameters<'db> { &self.parameters @@ -445,6 +496,18 @@ impl<'db> Parameters<'db> { ) } + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let value = self + .value + .iter() + .map(|param| param.apply_specialization(db, specialization)) + .collect(); + Self { + value, + is_gradual: self.is_gradual, + } + } + pub(crate) fn len(&self) -> usize { self.value.len() } @@ -606,6 +669,18 @@ impl<'db> Parameter<'db> { self } + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let annotated_type = self + .annotated_type + .map(|ty| ty.apply_specialization(db, specialization)); + let kind = self.kind.apply_specialization(db, specialization); + Self { + annotated_type, + kind, + form: self.form, + } + } + /// Strip information from the parameter so that two equivalent parameters compare equal. /// Normalize nested unions and intersections in the annotated type, if any. /// @@ -792,6 +867,27 @@ pub(crate) enum ParameterKind<'db> { }, } +impl<'db> ParameterKind<'db> { + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + match self { + Self::PositionalOnly { name, default_type } => Self::PositionalOnly { + name: name.clone(), + default_type: default_type.map(|ty| ty.apply_specialization(db, specialization)), + }, + Self::PositionalOrKeyword { name, default_type } => Self::PositionalOrKeyword { + name: name.clone(), + default_type: default_type.map(|ty| ty.apply_specialization(db, specialization)), + }, + Self::Variadic { name } => Self::Variadic { name: name.clone() }, + Self::KeywordOnly { name, default_type } => Self::KeywordOnly { + name: name.clone(), + default_type: default_type.map(|ty| ty.apply_specialization(db, specialization)), + }, + Self::KeywordVariadic { name } => Self::KeywordVariadic { name: name.clone() }, + } + } +} + /// Whether a parameter is used as a value or a type form. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum ParameterForm { From aa649909594fb157f293ab2a2dab844d2b22312d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 4 Apr 2025 15:00:21 -0400 Subject: [PATCH 049/100] Specialize generic base class in generic subclass --- .../resources/mdtest/generics/classes.md | 3 +- .../resources/mdtest/stubs/class.md | 3 +- crates/red_knot_python_semantic/src/types.rs | 6 +- .../src/types/class.rs | 161 +++++++++++------- .../src/types/class_base.rs | 7 +- .../src/types/infer.rs | 4 +- .../red_knot_python_semantic/src/types/mro.rs | 49 ++++-- .../src/types/slots.rs | 5 +- 8 files changed, 149 insertions(+), 89 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 5dd01a0c2525f..5364bb1c26cfd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -176,8 +176,7 @@ class Base[T]: class Sub[U](Base[U]): ... reveal_type(Base[int].x) # revealed: int | None -# TODO: revealed: int | None -reveal_type(Sub[int].x) # revealed: T | None +reveal_type(Sub[int].x) # revealed: int | None ``` ## Cyclic class definition diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md index bd971797386ad..2716eb0a98837 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md @@ -11,8 +11,7 @@ class Foo[T]: ... class Bar(Foo[Bar]): ... reveal_type(Bar) # revealed: Literal[Bar] -# TODO: Instead of `Literal[Foo]`, we might eventually want to show a type that involves the type parameter. -reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]] +reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Foo[Bar], Literal[object]] ``` ## Access to attributes declared in stubs diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 03a75982a1179..7c329c26a8626 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -931,7 +931,7 @@ impl<'db> Type<'db> { (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() - .is_some_and(|target_class| class.is_subclass_of(db, target_class)), + .is_some_and(|target_class| class.is_subclass_of(db, None, target_class)), (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() @@ -1439,7 +1439,7 @@ impl<'db> Type<'db> { | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => !class_b.is_subclass_of(db, class_a), + ClassBase::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), } } @@ -1979,7 +1979,7 @@ impl<'db> Type<'db> { "__get__" | "__set__" | "__delete__", ) => Some(Symbol::Unbound.into()), - _ => Some(class.class_member(db, name)), + _ => Some(class.class_member(db, None, name)), } } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 94b86b5d4f685..4f1542d0fdd9f 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -62,6 +62,7 @@ fn try_mro_cycle_recover<'db>( _value: &Result, MroError<'db>>, _count: u32, _self: ClassLiteralType<'db>, + _specialization: Option>, ) -> salsa::CycleRecoveryAction, MroError<'db>>> { salsa::CycleRecoveryAction::Iterate } @@ -70,8 +71,12 @@ fn try_mro_cycle_recover<'db>( fn try_mro_cycle_initial<'db>( db: &'db dyn Db, self_: ClassLiteralType<'db>, + specialization: Option>, ) -> Result, MroError<'db>> { - Ok(Mro::from_error(db, self_.default_specialization(db))) + Ok(Mro::from_error( + db, + self_.apply_optional_specialization(db, specialization), + )) } #[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] @@ -178,6 +183,21 @@ impl<'db> ClassType<'db> { } } + /// Returns the class literal and specialization for this class. For a non-generic class, this + /// is the class itself. For a generic alias, this is the alias's origin. + pub(crate) fn class_literal( + self, + db: &'db dyn Db, + ) -> (ClassLiteralType<'db>, Option>) { + match self { + Self::NonGeneric(non_generic) => (ClassLiteralType::NonGeneric(non_generic), None), + Self::Generic(generic) => ( + ClassLiteralType::Generic(generic.origin(db)), + Some(generic.specialization(db)), + ), + } + } + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { &self.class(db).name } @@ -214,13 +234,6 @@ impl<'db> ClassType<'db> { } } - fn specialize_class_base(&self, db: &'db dyn Db, base: ClassBase<'db>) -> ClassBase<'db> { - match self { - Self::NonGeneric(_) => base, - Self::Generic(generic) => base.apply_specialization(db, generic.specialization(db)), - } - } - fn specialize_type(&self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { Self::NonGeneric(_) => ty, @@ -238,15 +251,6 @@ impl<'db> ClassType<'db> { self.is_known(db, KnownClass::Object) } - /// Returns the class literal for this class. For a non-generic class, this is the class - /// itself. For a generic alias, this is the alias's origin. - pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> { - match self { - Self::NonGeneric(non_generic) => ClassLiteralType::NonGeneric(non_generic), - Self::Generic(generic) => ClassLiteralType::Generic(generic.origin(db)), - } - } - /// Iterate over the [method resolution order] ("MRO") of the class. /// /// If the MRO could not be accurately resolved, this method falls back to iterating @@ -256,14 +260,14 @@ impl<'db> ClassType<'db> { /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order pub(super) fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { - self.class_literal(db) - .iter_mro(db) - .map(move |base| self.specialize_class_base(db, base)) + let (class_literal, specialization) = self.class_literal(db); + class_literal.iter_mro(db, specialization) } /// Is this class final? pub(super) fn is_final(self, db: &'db dyn Db) -> bool { - self.class_literal(db).is_final(db) + let (class_literal, _) = self.class_literal(db); + class_literal.is_final(db) } /// Return `true` if `other` is present in this class's MRO. @@ -275,7 +279,8 @@ impl<'db> ClassType<'db> { /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - self.specialize_type(db, self.class_literal(db).metaclass(db)) + let (class_literal, _) = self.class_literal(db); + self.specialize_type(db, class_literal.metaclass(db)) } /// Return a type representing "the set of all instances of the metaclass of this class". @@ -292,8 +297,9 @@ impl<'db> ClassType<'db> { /// /// TODO: Should this be made private...? pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.class_literal(db) - .class_member(db, name) + let (class_literal, specialization) = self.class_literal(db); + class_literal + .class_member(db, specialization, name) .map_type(|ty| self.specialize_type(db, ty)) } @@ -304,7 +310,8 @@ impl<'db> ClassType<'db> { /// directly. Use [`Class::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.class_literal(db) + let (class_literal, _) = self.class_literal(db); + class_literal .own_class_member(db, name) .map_type(|ty| self.specialize_type(db, ty)) } @@ -316,15 +323,17 @@ impl<'db> ClassType<'db> { /// /// The attribute might also be defined in a superclass of this class. pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.class_literal(db) - .instance_member(db, name) + let (class_literal, specialization) = self.class_literal(db); + class_literal + .instance_member(db, specialization, name) .map_type(|ty| self.specialize_type(db, ty)) } /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.class_literal(db) + let (class_literal, _) = self.class_literal(db); + class_literal .own_instance_member(db, name) .map_type(|ty| self.specialize_type(db, ty)) } @@ -397,6 +406,23 @@ impl<'db> ClassLiteralType<'db> { FileRange::new(class.file(db), class.node(db).range) } + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> ClassType<'db> { + match (self, specialization) { + (Self::NonGeneric(non_generic), _) => ClassType::NonGeneric(non_generic), + (Self::Generic(generic), None) => { + let specialization = generic.generic_context(db).default_specialization(db); + ClassType::Generic(GenericAlias::new(db, generic, specialization)) + } + (Self::Generic(generic), Some(specialization)) => { + ClassType::Generic(GenericAlias::new(db, generic, specialization)) + } + } + } + /// Returns the default specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. @@ -492,10 +518,14 @@ impl<'db> ClassLiteralType<'db> { /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order #[salsa::tracked(return_ref, cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial)] - pub(super) fn try_mro(self, db: &'db dyn Db) -> Result, MroError<'db>> { + pub(super) fn try_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Result, MroError<'db>> { let class = self.class(db); tracing::trace!("ClassLiteralType::try_mro: {}", class.name); - Mro::of_class(db, self) + Mro::of_class(db, self, specialization) } /// Iterate over the [method resolution order] ("MRO") of the class. @@ -506,15 +536,25 @@ impl<'db> ClassLiteralType<'db> { /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { - MroIterator::new(db, self) + pub(super) fn iter_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> impl Iterator> { + MroIterator::new(db, self, specialization) } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + pub(super) fn is_subclass_of( + self, + db: &'db dyn Db, + specialization: Option>, + other: ClassType<'db>, + ) -> bool { // `is_subclass_of` is checking the subtype relation, in which gradual types do not // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db).contains(&ClassBase::Class(other)) + self.iter_mro(db, specialization) + .contains(&ClassBase::Class(other)) } /// Return the explicit `metaclass` of this class, if one is defined. @@ -575,7 +615,8 @@ impl<'db> ClassLiteralType<'db> { let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { (metaclass, self) } else if let Some(base_class) = base_classes.next() { - (base_class.metaclass(db), base_class.class_literal(db)) + let (base_class_literal, _) = base_class.class_literal(db); + (base_class.metaclass(db), base_class_literal) } else { (KnownClass::Type.to_class_literal(db), self) }; @@ -626,21 +667,23 @@ impl<'db> ClassLiteralType<'db> { continue; }; if metaclass.is_subclass_of(db, candidate.metaclass) { + let (base_class_literal, _) = base_class.class_literal(db); candidate = MetaclassCandidate { metaclass, - explicit_metaclass_of: base_class.class_literal(db), + explicit_metaclass_of: base_class_literal, }; continue; } if candidate.metaclass.is_subclass_of(db, metaclass) { continue; } + let (base_class_literal, _) = base_class.class_literal(db); return Err(MetaclassError { kind: MetaclassErrorKind::Conflict { candidate1: candidate, candidate2: MetaclassCandidate { metaclass, - explicit_metaclass_of: base_class.class_literal(db), + explicit_metaclass_of: base_class_literal, }, candidate1_is_base_class: explicit_metaclass.is_none(), }, @@ -655,9 +698,14 @@ impl<'db> ClassLiteralType<'db> { /// The member resolves to a member on the class itself or any of its proper superclasses. /// /// TODO: Should this be made private...? - pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(super) fn class_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> SymbolAndQualifiers<'db> { if name == "__mro__" { - let tuple_elements = self.iter_mro(db).map(Type::from); + let tuple_elements = self.iter_mro(db, specialization).map(Type::from); return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into(); } @@ -672,7 +720,7 @@ impl<'db> ClassLiteralType<'db> { let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound(TypeQualifiers::empty())); - for (idx, superclass) in self.iter_mro(db).enumerate() { + for superclass in self.iter_mro(db, specialization) { match superclass { ClassBase::Dynamic(DynamicType::TodoProtocol) => { // TODO: We currently skip `Protocol` when looking up class members, in order to @@ -687,15 +735,7 @@ impl<'db> ClassLiteralType<'db> { } ClassBase::Class(class) => { lookup_result = lookup_result.or_else(|lookup_error| { - let member = if idx == 0 { - // The first element of the MRO is always the class itself. For that - // class, we want to look up the member in the class literal, so that - // we don't apply the default specialization. - self.own_class_member(db, name) - } else { - class.own_class_member(db, name) - }; - lookup_error.or_fall_back_to(db, member) + lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) }); } } @@ -751,11 +791,16 @@ impl<'db> ClassLiteralType<'db> { /// defined attribute that is only present in a method (typically `__init__`). /// /// The attribute might also be defined in a superclass of this class. - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(super) fn instance_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> SymbolAndQualifiers<'db> { let mut union = UnionBuilder::new(db); let mut union_qualifiers = TypeQualifiers::empty(); - for (idx, superclass) in self.iter_mro(db).enumerate() { + for superclass in self.iter_mro(db, specialization) { match superclass { ClassBase::Dynamic(DynamicType::TodoProtocol) => { // TODO: We currently skip `Protocol` when looking up instance members, in order to @@ -768,18 +813,10 @@ impl<'db> ClassLiteralType<'db> { ); } ClassBase::Class(class) => { - let member = if idx == 0 { - // The first element of the MRO is always the class itself. For that - // class, we want to look up the member in the class literal, so that - // we don't apply the default specialization. - self.own_instance_member(db, name) - } else { - class.own_instance_member(db, name) - }; if let member @ SymbolAndQualifiers { symbol: Symbol::Type(ty, boundness), qualifiers, - } = member + } = class.own_instance_member(db, name) { // TODO: We could raise a diagnostic here if there are conflicting type qualifiers union_qualifiers |= qualifiers; @@ -1017,7 +1054,7 @@ impl<'db> ClassLiteralType<'db> { ) -> bool { let mut result = false; for explicit_base_class in class.fully_static_explicit_bases(db) { - let explicit_base_class_literal = explicit_base_class.class_literal(db); + let (explicit_base_class_literal, _) = explicit_base_class.class_literal(db); if !classes_on_stack.insert(explicit_base_class_literal) { return true; } @@ -1419,7 +1456,7 @@ impl<'db> KnownClass { /// *and* `class` is a subclass of `other`. pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { self.try_to_class_literal(db) - .is_ok_and(|class| class.is_subclass_of(db, other)) + .is_ok_and(|class| class.is_subclass_of(db, None, other)) } /// Return the module in which we should look up the definition for this class diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 93dffc2294698..97cb56291de6f 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -67,7 +67,12 @@ impl<'db> ClassBase<'db> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.base { ClassBase::Dynamic(dynamic) => dynamic.fmt(f), - ClassBase::Class(class) => write!(f, "", class.name(self.db)), + ClassBase::Class(class @ ClassType::NonGeneric(_)) => { + write!(f, "", class.name(self.db)) + } + ClassBase::Class(ClassType::Generic(alias)) => { + write!(f, "", alias.display(self.db)) + } } } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 8e74dd4f6a5be..e4b62e9dbbfef 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -755,7 +755,7 @@ impl<'db> TypeInferenceBuilder<'db> { } // (3) Check that the class's MRO is resolvable - match class.try_mro(self.db()).as_ref() { + match class.try_mro(self.db(), None).as_ref() { Err(mro_error) => { match mro_error.reason() { MroErrorKind::DuplicateBases(duplicates) => { @@ -4361,7 +4361,7 @@ impl<'db> TypeInferenceBuilder<'db> { LookupError::Unbound(_) => { let bound_on_instance = match value_type { Type::ClassLiteral(class) => { - !class.instance_member(db, attr).symbol.is_unbound() + !class.instance_member(db, None, attr).symbol.is_unbound() } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index aa2d9608006a5..8f3edc3ca938c 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -4,6 +4,7 @@ use std::ops::Deref; use rustc_hash::FxHashSet; use crate::types::class_base::ClassBase; +use crate::types::generics::Specialization; use crate::types::{ClassLiteralType, ClassType, Type}; use crate::Db; @@ -47,9 +48,11 @@ impl<'db> Mro<'db> { pub(super) fn of_class( db: &'db dyn Db, class: ClassLiteralType<'db>, + specialization: Option>, ) -> Result> { - Self::of_class_impl(db, class) - .map_err(|err| err.into_mro_error(db, class.default_specialization(db))) + Self::of_class_impl(db, class, specialization).map_err(|err| { + err.into_mro_error(db, class.apply_optional_specialization(db, specialization)) + }) } pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { @@ -63,6 +66,7 @@ impl<'db> Mro<'db> { fn of_class_impl( db: &'db dyn Db, class: ClassLiteralType<'db>, + specialization: Option>, ) -> Result> { let class_bases = class.explicit_bases(db); @@ -70,7 +74,10 @@ impl<'db> Mro<'db> { // We emit errors for cyclically defined classes elsewhere. // It's important that we don't even try to infer the MRO for a cyclically defined class, // or we'll end up in an infinite loop. - return Ok(Mro::from_error(db, class.default_specialization(db))); + return Ok(Mro::from_error( + db, + class.apply_optional_specialization(db, specialization), + )); } match class_bases { @@ -78,7 +85,7 @@ impl<'db> Mro<'db> { // the only class in Python that has an MRO with length <2 [] if class.is_object(db) => Ok(Self::from([ // object is not generic, so the default specialization should be a no-op - ClassBase::Class(class.default_specialization(db)), + ClassBase::Class(class.apply_optional_specialization(db, specialization)), ])), // All other classes in Python have an MRO with length >=2. @@ -95,7 +102,7 @@ impl<'db> Mro<'db> { // (, ) // ``` [] => Ok(Self::from([ - ClassBase::Class(class.default_specialization(db)), + ClassBase::Class(class.apply_optional_specialization(db, specialization)), ClassBase::object(db), ])), @@ -107,11 +114,11 @@ impl<'db> Mro<'db> { [single_base] => ClassBase::try_from_type(db, *single_base).map_or_else( || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))), |single_base| { - Ok( - std::iter::once(ClassBase::Class(class.default_specialization(db))) - .chain(single_base.mro(db)) - .collect(), - ) + Ok(std::iter::once(ClassBase::Class( + class.apply_optional_specialization(db, specialization), + )) + .chain(single_base.mro(db)) + .collect()) }, ), @@ -136,7 +143,7 @@ impl<'db> Mro<'db> { } let mut seqs = vec![VecDeque::from([ClassBase::Class( - class.default_specialization(db), + class.apply_optional_specialization(db, specialization), )])]; for base in &valid_bases { seqs.push(base.mro(db).collect()); @@ -152,7 +159,8 @@ impl<'db> Mro<'db> { .filter_map(|(index, base)| Some((index, base.into_class()?))) { if !seen_bases.insert(base) { - duplicate_bases.push((index, base.class_literal(db))); + let (base_class_literal, _) = base.class_literal(db); + duplicate_bases.push((index, base_class_literal)); } } @@ -219,6 +227,9 @@ pub(super) struct MroIterator<'db> { /// The class whose MRO we're iterating over class: ClassLiteralType<'db>, + /// The specialization to apply to each MRO element, if any + specialization: Option>, + /// Whether or not we've already yielded the first element of the MRO first_element_yielded: bool, @@ -231,10 +242,15 @@ pub(super) struct MroIterator<'db> { } impl<'db> MroIterator<'db> { - pub(super) fn new(db: &'db dyn Db, class: ClassLiteralType<'db>) -> Self { + pub(super) fn new( + db: &'db dyn Db, + class: ClassLiteralType<'db>, + specialization: Option>, + ) -> Self { Self { db, class, + specialization, first_element_yielded: false, subsequent_elements: None, } @@ -245,7 +261,7 @@ impl<'db> MroIterator<'db> { fn full_mro_except_first_element(&mut self) -> impl Iterator> + '_ { self.subsequent_elements .get_or_insert_with(|| { - let mut full_mro_iter = match self.class.try_mro(self.db) { + let mut full_mro_iter = match self.class.try_mro(self.db, self.specialization) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -262,7 +278,10 @@ impl<'db> Iterator for MroIterator<'db> { fn next(&mut self) -> Option { if !self.first_element_yielded { self.first_element_yielded = true; - return Some(ClassBase::Class(self.class.default_specialization(self.db))); + return Some(ClassBase::Class( + self.class + .apply_optional_specialization(self.db, self.specialization), + )); } self.full_mro_except_first_element().next() } diff --git a/crates/red_knot_python_semantic/src/types/slots.rs b/crates/red_knot_python_semantic/src/types/slots.rs index 19fac45d2463d..2b265372ad16d 100644 --- a/crates/red_knot_python_semantic/src/types/slots.rs +++ b/crates/red_knot_python_semantic/src/types/slots.rs @@ -66,12 +66,13 @@ pub(super) fn check_class_slots( continue; }; - let solid_base = base.iter_mro(db).find_map(|current| { + let solid_base = base.iter_mro(db, None).find_map(|current| { let ClassBase::Class(current) = current else { return None; }; - match SlotsKind::from(db, current.class_literal(db)) { + let (class_literal, _) = current.class_literal(db); + match SlotsKind::from(db, class_literal) { SlotsKind::NotEmpty => Some(current), SlotsKind::NotSpecified | SlotsKind::Empty => None, SlotsKind::Dynamic => None, From 7eb7c289a04c4203c3e0e35c211bbff11ca0c03e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 15:37:31 -0400 Subject: [PATCH 050/100] Only specialize function literals, not all callables --- crates/red_knot_python_semantic/src/types.rs | 189 ++++++++---------- .../src/types/class_base.rs | 4 - .../src/types/display.rs | 20 +- .../src/types/infer.rs | 7 +- .../src/types/signatures.rs | 6 +- .../src/types/type_ordering.rs | 4 - 6 files changed, 101 insertions(+), 129 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index cc0c18c25199e..e319f51e858e0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -241,6 +241,18 @@ pub struct PropertyInstanceType<'db> { setter: Option>, } +impl<'db> PropertyInstanceType<'db> { + fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let getter = self + .getter(db) + .map(|ty| ty.apply_specialization(db, specialization)); + let setter = self + .setter(db) + .map(|ty| ty.apply_specialization(db, specialization)); + Self::new(db, getter, setter) + } +} + /// Representation of a type: a set of possible values at runtime. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub enum Type<'db> { @@ -261,10 +273,6 @@ pub enum Type<'db> { /// the `self` parameter, and return a `MethodType & Callable[[int], str]`. /// One drawback would be that we could not show the bound instance when that type is displayed. BoundMethod(BoundMethodType<'db>), - /// Represents the specialization of a callable that has access to generic typevars, either - /// because it is itself a generic function, or because it appears in the body of a generic - /// class. - SpecializedCallable(SpecializedCallableType<'db>), /// Represents a specific instance of `types.MethodWrapperType`. /// /// TODO: consider replacing this with `Callable & types.MethodWrapperType` type? @@ -400,10 +408,6 @@ impl<'db> Type<'db> { .iter() .any(|ty| ty.contains_todo(db)), - Self::SpecializedCallable(specialized) => { - specialized.callable_type(db).contains_todo(db) - } - Self::Callable(callable) => { let signature = callable.signature(db); signature.parameters().iter().any(|param| { @@ -662,9 +666,6 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), - Type::SpecializedCallable(specialized) => { - Type::SpecializedCallable(specialized.normalized(db)) - } Type::LiteralString | Type::Instance(_) | Type::PropertyInstance(_) @@ -876,10 +877,6 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), - (Type::SpecializedCallable(specialized), _) => { - specialized.callable_type(db).is_subtype_of(db, target) - } - // The same reasoning applies for these special callable types: (Type::BoundMethod(_), _) => KnownClass::MethodType .to_instance(db) @@ -1596,17 +1593,6 @@ impl<'db> Type<'db> { .to_instance(db) .is_disjoint_from(db, other), - ( - Type::SpecializedCallable(self_specialized), - Type::SpecializedCallable(other_specialized), - ) => { - self_specialized - .callable_type(db) - .is_disjoint_from(db, other_specialized.callable_type(db)) - || self_specialized.specialization(db) != other_specialized.specialization(db) - } - (Type::SpecializedCallable(_), _) | (_, Type::SpecializedCallable(_)) => false, - (Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => { KnownClass::MethodWrapperType .to_instance(db) @@ -1748,9 +1734,6 @@ impl<'db> Type<'db> { .elements(db) .iter() .all(|elem| elem.is_fully_static(db)), - Type::SpecializedCallable(specialized) => { - specialized.callable_type(db).is_fully_static(db) - } Type::Callable(callable) => callable.is_fully_static(db), } } @@ -1814,9 +1797,6 @@ impl<'db> Type<'db> { // ``` false } - Type::SpecializedCallable(specialized) => { - specialized.callable_type(db).is_singleton(db) - } Type::MethodWrapper(_) => { // Just a special case of `BoundMethod` really // (this variant represents `f.__get__`, where `f` is any function) @@ -1886,10 +1866,6 @@ impl<'db> Type<'db> { .all(|constraint| constraint.is_single_valued(db)), }, - Type::SpecializedCallable(specialized) => { - specialized.callable_type(db).is_single_valued(db) - } - Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` false @@ -2023,13 +1999,6 @@ impl<'db> Type<'db> { .find_name_in_mro(db, name) } - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .find_name_in_mro(db, name) - .map(|sq| { - sq.map_type(|ty| ty.apply_specialization(db, specialized.specialization(db))) - }), - Type::FunctionLiteral(_) | Type::Callable(_) | Type::BoundMethod(_) @@ -2110,10 +2079,6 @@ impl<'db> Type<'db> { Type::BoundMethod(_) => KnownClass::MethodType .to_instance(db) .instance_member(db, name), - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .instance_member(db, name) - .map_type(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::MethodWrapper(_) => KnownClass::MethodWrapperType .to_instance(db) .instance_member(db, name), @@ -2511,10 +2476,6 @@ impl<'db> Type<'db> { }) } }, - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .member_lookup_with_policy(db, name, policy) - .map_type(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::MethodWrapper(_) => KnownClass::MethodWrapperType .to_instance(db) .member(db, &name), @@ -2812,10 +2773,6 @@ impl<'db> Type<'db> { Type::AlwaysFalsy => Truthiness::AlwaysFalse, - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .try_bool_impl(db, allow_short_circuit)?, - Type::ClassLiteral(class) => class .metaclass_instance_type(db) .try_bool_impl(db, allow_short_circuit)?, @@ -2934,11 +2891,6 @@ impl<'db> Type<'db> { Signatures::single(signature) } - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .signatures(db) - .apply_specialization(db, specialized.specialization(db)), - Type::MethodWrapper( MethodWrapperKind::FunctionTypeDunderGet(_) | MethodWrapperKind::PropertyDunderGet(_), @@ -3612,10 +3564,6 @@ impl<'db> Type<'db> { Some(builder.build()) } Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance()")), - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .to_instance(db) - .map(|ty| ty.apply_specialization(db, specialized.specialization(db))), Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::FunctionLiteral(_) @@ -3698,11 +3646,6 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }), - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .in_type_expression(db) - .map(|ty| ty.apply_specialization(db, specialized.specialization(db))), - Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), KnownInstanceType::Never | KnownInstanceType::NoReturn => Ok(Type::Never), @@ -3900,10 +3843,6 @@ impl<'db> Type<'db> { Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::SpecializedCallable(specialized) => specialized - .callable_type(db) - .to_meta_type(db) - .apply_specialization(db, specialized.specialization(db)), Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), Type::Callable(_) => KnownClass::Type.to_instance(db), @@ -3959,16 +3898,36 @@ impl<'db> Type<'db> { match self { Type::TypeVar(typevar) => specialization.get(db, typevar).unwrap_or(self), - // Callables need this extra wrapper that will apply the specialization to the - // callable's parameters and return types. That is done lazily if and when the callable - // is actually called. - Type::FunctionLiteral(_) - | Type::Callable(_) - | Type::BoundMethod(_) - | Type::SpecializedCallable(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) => { - Type::SpecializedCallable(SpecializedCallableType::new(db, self, specialization)) + Type::FunctionLiteral(function) => { + Type::FunctionLiteral(function.apply_specialization(db, specialization)) + } + + Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( + db, + method.function(db).apply_specialization(db, specialization), + method.self_instance(db), + )), + + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet( + function.apply_specialization(db, specialization), + )) + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet( + property.apply_specialization(db, specialization), + )) + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet( + property.apply_specialization(db, specialization), + )) + } + + Type::Callable(callable) => { + Type::Callable(callable.apply_specialization(db, specialization)) } Type::GenericAlias(generic) => { @@ -4007,6 +3966,7 @@ impl<'db> Type<'db> { | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy + | Type::WrapperDescriptor(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -4072,7 +4032,6 @@ impl<'db> Type<'db> { Self::BoundMethod(method) => { Some(TypeDefinition::Function(method.function(db).definition(db))) } - Self::SpecializedCallable(specialized) => specialized.callable_type(db).definition(db), Self::FunctionLiteral(function) => { Some(TypeDefinition::Function(function.definition(db))) } @@ -5007,6 +4966,11 @@ pub struct FunctionType<'db> { /// A set of special decorators that were applied to this function decorators: FunctionDecorators, + + /// A specialization that should be applied to the function's parameter and return types, + /// either because the function is itself generic, or because it appears in the body of a + /// generic class. + specialization: Option>, } #[salsa::tracked] @@ -5019,7 +4983,12 @@ impl<'db> FunctionType<'db> { /// /// This powers the `CallableTypeOf` special form from the `knot_extensions` module. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - Type::Callable(CallableType::new(db, self.signature(db).clone())) + let signature = if let Some(specialization) = self.specialization(db) { + self.signature(db).apply_specialization(db, specialization) + } else { + self.signature(db).clone() + }; + Type::Callable(CallableType::new(db, signature)) } /// Returns the [`FileRange`] of the function's name. @@ -5060,10 +5029,14 @@ impl<'db> FunctionType<'db> { let internal_signature = self.internal_signature(db); if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - Signature::todo("return type of overloaded function") - } else { - internal_signature + return Signature::todo("return type of overloaded function"); + } + + if let Some(specialization) = self.specialization(db) { + return internal_signature.apply_specialization(db, specialization); } + + internal_signature } /// Typed internally-visible signature for this function. @@ -5086,6 +5059,21 @@ impl<'db> FunctionType<'db> { pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { self.known(db) == Some(known_function) } + + fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let specialization = match self.specialization(db) { + Some(existing) => existing.apply_specialization(db, specialization), + None => specialization, + }; + Self::new( + db, + self.name(db).clone(), + self.known(db), + self.body_scope(db), + self.decorators(db), + Some(specialization), + ) + } } /// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might @@ -5218,26 +5206,6 @@ impl<'db> BoundMethodType<'db> { } } -/// Represents the specialization of a callable that has access to generic typevars, either because -/// it is itself a generic function, or because it appears in the body of a generic class. -#[salsa::tracked(debug)] -pub struct SpecializedCallableType<'db> { - /// The callable that has been specialized. (Note that this is not [`CallableType`] since there - /// are other types that are callable.) - pub callable_type: Type<'db>, - - /// The specialization of any generic typevars that are visible to the callable. - pub specialization: Specialization<'db>, -} - -impl<'db> SpecializedCallableType<'db> { - fn normalized(self, db: &'db dyn Db) -> Self { - let callable_type = self.callable_type(db).normalized(db); - let specialization = self.specialization(db).normalized(db); - SpecializedCallableType::new(db, callable_type, specialization) - } -} - /// This type represents the set of all callable objects with a certain signature. /// It can be written in type expressions using `typing.Callable`. /// `lambda` expressions are inferred directly as `CallableType`s; all function-literal types @@ -5273,6 +5241,11 @@ impl<'db> CallableType<'db> { CallableType::new(db, Signature::new(parameters, return_ty)) } + fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let signature = self.signature(db).apply_specialization(db, specialization); + Self::new(db, signature) + } + /// Returns `true` if this is a fully static callable type. /// /// A callable type is fully static if all of its parameters and return type are fully static diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 41446e4a81737..0e57a0566079f 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -122,10 +122,6 @@ impl<'db> ClassBase<'db> { | Type::TypeVar(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => None, - Type::SpecializedCallable(specialized) => { - Self::try_from_type(db, specialized.callable_type(db)) - .map(|ty| ty.apply_specialization(db, specialized.specialization(db))) - } Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index cfb1ad7ac1a4c..8dce3658c6420 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -93,7 +93,18 @@ impl Display for DisplayRepresentation<'_> { ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), - Type::FunctionLiteral(function) => f.write_str(function.name(self.db)), + Type::FunctionLiteral(function) => { + if let Some(specialization) = function.specialization(self.db) { + write!( + f, + "", + name = function.name(self.db), + specialization = specialization.display(self.db), + ) + } else { + f.write_str(function.name(self.db)) + } + } Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f), Type::BoundMethod(bound_method) => { write!( @@ -103,13 +114,6 @@ impl Display for DisplayRepresentation<'_> { instance = bound_method.self_instance(self.db).display(self.db) ) } - Type::SpecializedCallable(specialized) => { - write!( - f, - "", - callable = specialized.callable_type(self.db).display(self.db), - ) - } Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { write!( f, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e4b62e9dbbfef..239b7a1f2c4ed 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1430,12 +1430,15 @@ impl<'db> TypeInferenceBuilder<'db> { .node_scope(NodeWithScopeRef::Function(function)) .to_scope_id(self.db(), self.file()); + let specialization = None; + let mut inferred_ty = Type::FunctionLiteral(FunctionType::new( self.db(), &name.id, function_kind, body_scope, function_decorators, + specialization, )); for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { @@ -2367,7 +2370,6 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::FunctionLiteral(..) | Type::Callable(..) | Type::BoundMethod(_) - | Type::SpecializedCallable(_) | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::TypeVar(..) @@ -4472,7 +4474,6 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::BoundMethod(_) - | Type::SpecializedCallable(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::GenericAlias(_) @@ -4748,7 +4749,6 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::SpecializedCallable(_) | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::ModuleLiteral(_) @@ -4771,7 +4771,6 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::SpecializedCallable(_) | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::ModuleLiteral(_) diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index ef9d7f90c15a0..5977ad496c831 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -301,7 +301,11 @@ impl<'db> Signature<'db> { } } - fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + pub(crate) fn apply_specialization( + &self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { let parameters = self.parameters.apply_specialization(db, specialization); let return_ty = self .return_ty diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 58dfb91245ff6..c3f8177266313 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -62,10 +62,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::BoundMethod(_), _) => Ordering::Less, (_, Type::BoundMethod(_)) => Ordering::Greater, - (Type::SpecializedCallable(left), Type::SpecializedCallable(right)) => left.cmp(right), - (Type::SpecializedCallable(_), _) => Ordering::Less, - (_, Type::SpecializedCallable(_)) => Ordering::Greater, - (Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right), (Type::MethodWrapper(_), _) => Ordering::Less, (_, Type::MethodWrapper(_)) => Ordering::Greater, From adb4abaa77d723a2b770c39da4f5168a4157ceb7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 15:53:07 -0400 Subject: [PATCH 051/100] Fix descriptor protocol tests --- .../resources/mdtest/generics/scoping.md | 43 +++++++++++++++++-- crates/red_knot_python_semantic/src/types.rs | 5 --- .../src/types/display.rs | 25 ++++++++--- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 7cdf663a25e2f..71526cd302ac5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -82,14 +82,50 @@ class C[T]: def m2(self, x: T) -> T: return x -c: C[int] = C() +c: C[int] = C[int]() c.m1(1) c.m2(1) -# TODO: expected type `int` -# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `T`" +# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `int`" c.m2("string") ``` +## Functions on generic classes are descriptors + +This repeats the tests in the [Functions as descriptors](./call/methods.md) test suite, but on a +generic class. This ensures that we are carrying through any specializations through the entirety of +the descriptor protocol, which is how `self` parameters are bound to instance methods. + +```py +from inspect import getattr_static + +class C[T]: + def f(self, x: T) -> str: + return "a" + +reveal_type(getattr_static(C[int], "f")) # revealed: Literal[] +reveal_type(getattr_static(C[int], "f").__get__) # revealed: +reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[] +reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int])) # revealed: + +reveal_type(C[int].f) # revealed: Literal[] +reveal_type(C[int]().f) # revealed: + +bound_method = C[int]().f +reveal_type(bound_method.__self__) # revealed: C[int] +reveal_type(bound_method.__func__) # revealed: Literal[] + +reveal_type(C[int]().f(1)) # revealed: str +reveal_type(bound_method(1)) # revealed: str + +C[int].f(1) # error: [missing-argument] +reveal_type(C[int].f(C[int](), 1)) # revealed: str + +class D[U](C[U]): + pass + +reveal_type(D[int]().f) # revealed: +``` + ## Methods can mention other typevars > A type variable used in a method that does not match any of the variables that parameterize the @@ -123,7 +159,6 @@ c: C[int] = C() # TODO: no errors # TODO: revealed: str # error: [invalid-argument-type] -# error: [invalid-argument-type] reveal_type(c.m(1, "string")) # revealed: S ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e319f51e858e0..ec8687daab8dc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3297,11 +3297,6 @@ impl<'db> Type<'db> { Type::GenericAlias(_) => { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` - eprintln!( - "==> instantiate generic alias {} to {}", - self.display(db), - self.to_instance(db).unwrap_or(Type::unknown()).display(db) - ); let signature = CallableSignature::single( self, Signature::new(Parameters::gradual_form(), self.to_instance(db)), diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 8dce3658c6420..8de021198fd07 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -97,7 +97,7 @@ impl Display for DisplayRepresentation<'_> { if let Some(specialization) = function.specialization(self.db) { write!( f, - "", + "<{name} specialized with {specialization}>", name = function.name(self.db), specialization = specialization.display(self.db), ) @@ -107,18 +107,31 @@ impl Display for DisplayRepresentation<'_> { } Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f), Type::BoundMethod(bound_method) => { + let function = bound_method.function(self.db); write!( f, - "", - method = bound_method.function(self.db).name(self.db), - instance = bound_method.self_instance(self.db).display(self.db) + "", + method = function.name(self.db), + instance = bound_method.self_instance(self.db).display(self.db), + specialization = if let Some(specialization) = function.specialization(self.db) + { + format!(" specialized with {}", specialization.display(self.db)) + } else { + "".to_string() + }, ) } Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { write!( f, - "", - function = function.name(self.db) + "", + function = function.name(self.db), + specialization = if let Some(specialization) = function.specialization(self.db) + { + format!(" specialized with {}", specialization.display(self.db)) + } else { + "".to_string() + }, ) } Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { From fd7914aa1ed9514ea72f5d45025462426277440f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 15:58:18 -0400 Subject: [PATCH 052/100] Remove unused methods --- .../src/types/class.rs | 17 -------- .../src/types/class_base.rs | 25 ------------ .../src/types/signatures.rs | 39 ------------------- 3 files changed, 81 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 8d8ddab371b07..3c17ef335e690 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -208,23 +208,6 @@ impl<'db> ClassType<'db> { self.class(db).definition(db) } - pub(crate) fn apply_specialization( - self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) -> Self { - match self { - Self::NonGeneric(_) => self, - Self::Generic(generic) => Self::Generic(GenericAlias::new( - db, - generic.origin(db), - generic - .specialization(db) - .apply_specialization(db, specialization), - )), - } - } - fn specialize_type(&self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { Self::NonGeneric(_) => ty, diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 0e57a0566079f..f4496692b6c69 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -1,4 +1,3 @@ -use crate::types::generics::Specialization; use crate::types::{todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, Type}; use crate::Db; use itertools::Either; @@ -33,30 +32,6 @@ impl<'db> ClassBase<'db> { } } - /// Applies a specialization to this class base. This is used, for instance, when a generic - /// class inherits from a generic alias: - /// - /// ```py - /// class A[T]: ... - /// class B[U](A[U]): ... - /// ``` - /// - /// `B` is a generic class, whose MRO includes the generic alias `A[U]`. If `B` is specialized - /// to `B[int]`, with specialization `{U: int}`, we can apply that specialization to the base - /// class. That results in `A[int]`, which is the corresponding entry in the MRO of `B[int]`. - pub(crate) fn apply_specialization( - self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) -> Self { - match self { - ClassBase::Dynamic(_) => self, - ClassBase::Class(class) => { - ClassBase::Class(class.apply_specialization(db, specialization)) - } - } - } - pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct Display<'db> { base: ClassBase<'db>, diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 5977ad496c831..27f502552cf95 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -67,25 +67,6 @@ impl<'db> Signatures<'db> { } } - pub(crate) fn apply_specialization( - &self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) -> Self { - let callable_type = self.callable_type.apply_specialization(db, specialization); - let signature_type = self.signature_type.apply_specialization(db, specialization); - let elements = self - .elements - .iter() - .map(|callable| callable.apply_specialization(db, specialization)) - .collect(); - Self { - callable_type, - signature_type, - elements, - } - } - pub(crate) fn iter(&self) -> std::slice::Iter<'_, CallableSignature<'db>> { self.elements.iter() } @@ -203,26 +184,6 @@ impl<'db> CallableSignature<'db> { self } - fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let callable_type = self.callable_type.apply_specialization(db, specialization); - let signature_type = self.signature_type.apply_specialization(db, specialization); - let bound_type = self - .bound_type - .map(|ty| ty.apply_specialization(db, specialization)); - let overloads = self - .overloads - .iter() - .map(|overload| overload.apply_specialization(db, specialization)) - .collect(); - Self { - callable_type, - signature_type, - dunder_call_is_possibly_unbound: self.dunder_call_is_possibly_unbound, - bound_type, - overloads, - } - } - pub(crate) fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> { self.overloads.iter() } From 7ebda98105d96474e1df0bfcd7ac005ee8936724 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 16:00:08 -0400 Subject: [PATCH 053/100] clippy --- crates/red_knot_python_semantic/src/types/class.rs | 8 ++++---- crates/red_knot_python_semantic/src/types/class_base.rs | 2 +- crates/red_knot_python_semantic/src/types/display.rs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 3c17ef335e690..3720ff8a2b9b5 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -208,7 +208,7 @@ impl<'db> ClassType<'db> { self.class(db).definition(db) } - fn specialize_type(&self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn specialize_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { Self::NonGeneric(_) => ty, Self::Generic(generic) => ty.apply_specialization(db, generic.specialization(db)), @@ -216,7 +216,7 @@ impl<'db> ClassType<'db> { } /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(&self, db: &'db dyn Db, known_class: KnownClass) -> bool { + pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { self.class(db).known == Some(known_class) } @@ -356,7 +356,7 @@ impl<'db> ClassLiteralType<'db> { } /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(&self, db: &'db dyn Db, known_class: KnownClass) -> bool { + pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { self.class(db).known == Some(known_class) } @@ -1347,7 +1347,7 @@ impl<'db> KnownClass { pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) .into_class_type() - .map(|class| Type::instance(class)) + .map(Type::instance) .unwrap_or_else(Type::unknown) } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index f4496692b6c69..26e563316a08a 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -60,7 +60,7 @@ impl<'db> ClassBase<'db> { KnownClass::Object .to_class_literal(db) .into_class_type() - .map_or(Self::unknown(), |class| Self::Class(class)) + .map_or(Self::unknown(), Self::Class) } /// Attempt to resolve `ty` into a `ClassBase`. diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 8de021198fd07..96083462bf1ff 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -117,7 +117,7 @@ impl Display for DisplayRepresentation<'_> { { format!(" specialized with {}", specialization.display(self.db)) } else { - "".to_string() + String::new() }, ) } @@ -130,7 +130,7 @@ impl Display for DisplayRepresentation<'_> { { format!(" specialized with {}", specialization.display(self.db)) } else { - "".to_string() + String::new() }, ) } @@ -249,7 +249,7 @@ pub struct DisplaySpecialization<'db> { impl Display for DisplaySpecialization<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_char('{')?; - for (idx, (var, ty)) in self.typevars.into_iter().zip(self.types).enumerate() { + for (idx, (var, ty)) in self.typevars.iter().zip(self.types).enumerate() { if idx > 0 { f.write_str(", ")?; } From 6257e89e4d14ecb5454ee495910a772cd19ce1f2 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 16:05:08 -0400 Subject: [PATCH 054/100] Apply specialization to signature in-place --- crates/red_knot_python_semantic/src/types.rs | 16 ++--- .../src/types/signatures.rs | 59 ++++++------------- 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3d7ce830ccc48..175d02705cbc5 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4982,11 +4982,10 @@ impl<'db> FunctionType<'db> { /// /// This powers the `CallableTypeOf` special form from the `knot_extensions` module. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - let signature = if let Some(specialization) = self.specialization(db) { - self.signature(db).apply_specialization(db, specialization) - } else { - self.signature(db).clone() - }; + let mut signature = self.signature(db).clone(); + if let Some(specialization) = self.specialization(db) { + signature.apply_specialization(db, specialization); + } Type::Callable(CallableType::new(db, signature)) } @@ -5025,14 +5024,14 @@ impl<'db> FunctionType<'db> { /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(return_ref)] pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { - let internal_signature = self.internal_signature(db); + let mut internal_signature = self.internal_signature(db); if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { return Signature::todo("return type of overloaded function"); } if let Some(specialization) = self.specialization(db) { - return internal_signature.apply_specialization(db, specialization); + internal_signature.apply_specialization(db, specialization); } internal_signature @@ -5241,7 +5240,8 @@ impl<'db> CallableType<'db> { } fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let signature = self.signature(db).apply_specialization(db, specialization); + let mut signature = self.signature(db).clone(); + signature.apply_specialization(db, specialization); Self::new(db, signature) } diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 27f502552cf95..6a1091398f88d 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -263,18 +263,14 @@ impl<'db> Signature<'db> { } pub(crate) fn apply_specialization( - &self, + &mut self, db: &'db dyn Db, specialization: Specialization<'db>, - ) -> Self { - let parameters = self.parameters.apply_specialization(db, specialization); - let return_ty = self + ) { + self.parameters.apply_specialization(db, specialization); + self.return_ty = self .return_ty .map(|ty| ty.apply_specialization(db, specialization)); - Self { - parameters, - return_ty, - } } /// Return the parameters in this signature. @@ -461,16 +457,10 @@ impl<'db> Parameters<'db> { ) } - fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let value = self - .value - .iter() - .map(|param| param.apply_specialization(db, specialization)) - .collect(); - Self { - value, - is_gradual: self.is_gradual, - } + fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { + self.value + .iter_mut() + .for_each(|param| param.apply_specialization(db, specialization)); } pub(crate) fn len(&self) -> usize { @@ -634,16 +624,11 @@ impl<'db> Parameter<'db> { self } - fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let annotated_type = self + fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { + self.annotated_type = self .annotated_type .map(|ty| ty.apply_specialization(db, specialization)); - let kind = self.kind.apply_specialization(db, specialization); - Self { - annotated_type, - kind, - form: self.form, - } + self.kind.apply_specialization(db, specialization); } /// Strip information from the parameter so that two equivalent parameters compare equal. @@ -833,22 +818,14 @@ pub(crate) enum ParameterKind<'db> { } impl<'db> ParameterKind<'db> { - fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { match self { - Self::PositionalOnly { name, default_type } => Self::PositionalOnly { - name: name.clone(), - default_type: default_type.map(|ty| ty.apply_specialization(db, specialization)), - }, - Self::PositionalOrKeyword { name, default_type } => Self::PositionalOrKeyword { - name: name.clone(), - default_type: default_type.map(|ty| ty.apply_specialization(db, specialization)), - }, - Self::Variadic { name } => Self::Variadic { name: name.clone() }, - Self::KeywordOnly { name, default_type } => Self::KeywordOnly { - name: name.clone(), - default_type: default_type.map(|ty| ty.apply_specialization(db, specialization)), - }, - Self::KeywordVariadic { name } => Self::KeywordVariadic { name: name.clone() }, + Self::PositionalOnly { default_type, .. } + | Self::PositionalOrKeyword { default_type, .. } + | Self::KeywordOnly { default_type, .. } => { + *default_type = default_type.map(|ty| ty.apply_specialization(db, specialization)); + } + Self::Variadic { .. } | Self::KeywordVariadic { .. } => {} } } } From 78cd92ae8e60553b8afdd6f083acc5e52f7339a6 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 16:06:47 -0400 Subject: [PATCH 055/100] typo --- .../resources/mdtest/generics/scoping.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 71526cd302ac5..f279074c23812 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -92,8 +92,8 @@ c.m2("string") ## Functions on generic classes are descriptors This repeats the tests in the [Functions as descriptors](./call/methods.md) test suite, but on a -generic class. This ensures that we are carrying through any specializations through the entirety of -the descriptor protocol, which is how `self` parameters are bound to instance methods. +generic class. This ensures that we are carrying any specializations through the entirety of the +descriptor protocol, which is how `self` parameters are bound to instance methods. ```py from inspect import getattr_static From 530e2bc16ffa3b3f3ab1c5acb3670f70e61bd7fc Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 16:27:07 -0400 Subject: [PATCH 056/100] Fix test case --- .../resources/mdtest/generics/scoping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index f279074c23812..bf6ffa7d3545b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -123,7 +123,7 @@ reveal_type(C[int].f(C[int](), 1)) # revealed: str class D[U](C[U]): pass -reveal_type(D[int]().f) # revealed: +reveal_type(D[int]().f) # revealed: ``` ## Methods can mention other typevars From 637fd484851eaf23cecdc75336f972c090dba549 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 16:48:54 -0400 Subject: [PATCH 057/100] lint --- .../resources/mdtest/generics/scoping.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index bf6ffa7d3545b..16cc691cf8c37 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -105,7 +105,9 @@ class C[T]: reveal_type(getattr_static(C[int], "f")) # revealed: Literal[] reveal_type(getattr_static(C[int], "f").__get__) # revealed: reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[] -reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int])) # revealed: +reveal_type( + getattr_static(C[int], "f").__get__(C[int](), C[int]) +) # revealed: reveal_type(C[int].f) # revealed: Literal[] reveal_type(C[int]().f) # revealed: From ec5a58887076c2d104b3b95ed8afa0f97c798ec5 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:13:47 -0400 Subject: [PATCH 058/100] Defer class definitions with string literals in base classes --- .../src/types/infer.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index f02db4255c24b..d4567dc418b7e 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -37,6 +37,7 @@ use itertools::{Either, Itertools}; use ruff_db::diagnostic::{DiagnosticId, Severity}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; +use ruff_python_ast::visitor::{walk_expr, Visitor}; use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -1735,7 +1736,12 @@ impl<'db> TypeInferenceBuilder<'db> { // Inference of bases deferred in stubs // TODO also defer stringified generic type parameters - if self.are_all_types_deferred() { + if self.are_all_types_deferred() + || class_node + .bases() + .iter() + .any(|base| contains_string_literal(base)) + { self.types.deferred.insert(definition); } else { for base in class_node.bases() { @@ -7492,6 +7498,21 @@ impl StringPartsCollector { } } +fn contains_string_literal(expr: &ast::Expr) -> bool { + struct ContainsStringLiteral(bool); + + impl<'a> Visitor<'a> for ContainsStringLiteral { + fn visit_expr(&mut self, expr: &'a ast::Expr) { + self.0 |= matches!(expr, ast::Expr::StringLiteral(_)); + walk_expr(self, expr); + } + } + + let mut visitor = ContainsStringLiteral(false); + visitor.visit_expr(expr); + visitor.0 +} + #[cfg(test)] mod tests { use crate::db::tests::{setup_db, TestDb}; From 27cb20838fc48ccbec3886314247e1d75adf2d7e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:15:11 -0400 Subject: [PATCH 059/100] fix lint fix --- .../resources/mdtest/generics/scoping.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 16cc691cf8c37..54b71825d2368 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -105,9 +105,10 @@ class C[T]: reveal_type(getattr_static(C[int], "f")) # revealed: Literal[] reveal_type(getattr_static(C[int], "f").__get__) # revealed: reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[] +# revealed: reveal_type( getattr_static(C[int], "f").__get__(C[int](), C[int]) -) # revealed: +) reveal_type(C[int].f) # revealed: Literal[] reveal_type(C[int]().f) # revealed: From ea125488ac59d27620144dafcced3050c1da2775 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:26:53 -0400 Subject: [PATCH 060/100] skip failing test for now --- .../resources/mdtest/properties.md | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/properties.md b/crates/red_knot_python_semantic/resources/mdtest/properties.md index de16979c66ca8..a6dfd275bfcd4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/properties.md +++ b/crates/red_knot_python_semantic/resources/mdtest/properties.md @@ -196,10 +196,14 @@ reveal_type(c.attr) # revealed: Unknown ## Behind the scenes +> TODO: This test is currently disabled pending [an upstream Salsa +> fix](https://github.com/salsa-rs/salsa/pull/741). Once that has been merged, re-enable this test +> by changing the language codes below back to `py`. + In this section, we trace through some of the steps that make properties work. We start with a simple class `C` and a property `attr`: -```py +```ignore class C: def __init__(self): self._attr: int = 0 @@ -216,7 +220,7 @@ class C: Next, we create an instance of `C`. As we have seen above, accessing `attr` on the instance will return an `int`: -```py +```ignore c = C() reveal_type(c.attr) # revealed: int @@ -226,7 +230,7 @@ Behind the scenes, when we write `c.attr`, the first thing that happens is that up the symbol `attr` on the meta-type of `c`, i.e. the class `C`. We can emulate this static lookup using `inspect.getattr_static`, to see that `attr` is actually an instance of the `property` class: -```py +```ignore from inspect import getattr_static attr_property = getattr_static(C, "attr") @@ -237,7 +241,7 @@ The `property` class has a `__get__` method, which makes it a descriptor. It als method, which means that it is a *data* descriptor (if there is no setter, `__set__` is still available but yields an `AttributeError` at runtime). -```py +```ignore reveal_type(type(attr_property).__get__) # revealed: reveal_type(type(attr_property).__set__) # revealed: ``` @@ -246,14 +250,14 @@ When we access `c.attr`, the `__get__` method of the `property` class is called, property object itself as the first argument, and the class instance `c` as the second argument. The third argument is the "owner" which can be set to `None` or to `C` in this case: -```py +```ignore reveal_type(type(attr_property).__get__(attr_property, c, C)) # revealed: int reveal_type(type(attr_property).__get__(attr_property, c, None)) # revealed: int ``` Alternatively, the above can also be written as a method call: -```py +```ignore reveal_type(attr_property.__get__(c, C)) # revealed: int ``` @@ -261,7 +265,7 @@ When we access `attr` on the class itself, the descriptor protocol is also invok argument is set to `None`. When `instance` is `None`, the call to `property.__get__` returns the property instance itself. So the following expressions are all equivalent -```py +```ignore reveal_type(attr_property) # revealed: property reveal_type(C.attr) # revealed: property reveal_type(attr_property.__get__(None, C)) # revealed: property @@ -271,7 +275,7 @@ reveal_type(type(attr_property).__get__(attr_property, None, C)) # revealed: pr When we set the property using `c.attr = "a"`, the `__set__` method of the property class is called. This attribute access desugars to -```py +```ignore type(attr_property).__set__(attr_property, c, "a") # error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed" @@ -280,7 +284,7 @@ type(attr_property).__set__(attr_property, c, 1) which is also equivalent to the following expressions: -```py +```ignore attr_property.__set__(c, "a") # error: [call-non-callable] attr_property.__set__(c, 1) @@ -293,7 +297,7 @@ C.attr.__set__(c, 1) Properties also have `fget` and `fset` attributes that can be used to retrieve the original getter and setter functions, respectively. -```py +```ignore reveal_type(attr_property.fget) # revealed: Literal[attr] reveal_type(attr_property.fget(c)) # revealed: int From c376dad1b6a862d31305a1ea5dd893dcfb9f33a6 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:29:04 -0400 Subject: [PATCH 061/100] clippy and lint --- .../resources/mdtest/generics/scoping.md | 4 +--- .../red_knot_python_semantic/resources/mdtest/properties.md | 6 +++--- crates/red_knot_python_semantic/src/types/infer.rs | 5 +---- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 54b71825d2368..bb7660f3241c5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -106,9 +106,7 @@ reveal_type(getattr_static(C[int], "f")) # revealed: Literal[ reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[] # revealed: -reveal_type( - getattr_static(C[int], "f").__get__(C[int](), C[int]) -) +reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int])) reveal_type(C[int].f) # revealed: Literal[] reveal_type(C[int]().f) # revealed: diff --git a/crates/red_knot_python_semantic/resources/mdtest/properties.md b/crates/red_knot_python_semantic/resources/mdtest/properties.md index a6dfd275bfcd4..dd0473b44f795 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/properties.md +++ b/crates/red_knot_python_semantic/resources/mdtest/properties.md @@ -196,9 +196,9 @@ reveal_type(c.attr) # revealed: Unknown ## Behind the scenes -> TODO: This test is currently disabled pending [an upstream Salsa -> fix](https://github.com/salsa-rs/salsa/pull/741). Once that has been merged, re-enable this test -> by changing the language codes below back to `py`. +> TODO: This test is currently disabled pending +> [an upstream Salsa fix](https://github.com/salsa-rs/salsa/pull/741). Once that has been merged, +> re-enable this test by changing the language codes below back to `py`. In this section, we trace through some of the steps that make properties work. We start with a simple class `C` and a property `attr`: diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d4567dc418b7e..e73017e04c2e5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1737,10 +1737,7 @@ impl<'db> TypeInferenceBuilder<'db> { // Inference of bases deferred in stubs // TODO also defer stringified generic type parameters if self.are_all_types_deferred() - || class_node - .bases() - .iter() - .any(|base| contains_string_literal(base)) + || class_node.bases().iter().any(contains_string_literal) { self.types.deferred.insert(definition); } else { From 311dc5967c9723565e2d69ee8ec090c1844e8e91 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:33:50 -0400 Subject: [PATCH 062/100] add xfail for specializing to union of constraints --- .../resources/mdtest/generics/classes.md | 4 ++++ 1 file changed, 4 insertions(+) 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 5364bb1c26cfd..0517ca7a5a398 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -106,6 +106,10 @@ reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] reveal_type(Constrained[str]()) # revealed: Constrained[str] +# TODO: error: [invalid-argument-type] +# TODO: revealed: Unknown +reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] + # error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`" reveal_type(Constrained[object]()) # revealed: Unknown ``` From 5fc8425493f596180946941dca6015988ee78750 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:41:33 -0400 Subject: [PATCH 063/100] specialize property types --- crates/red_knot_python_semantic/src/types.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 175d02705cbc5..2625a278953da 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3936,6 +3936,10 @@ impl<'db> Type<'db> { Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) } + Type::PropertyInstance(property) => { + Type::PropertyInstance(property.apply_specialization(db, specialization)) + } + Type::Union(union) => union.map(db, |element| { element.apply_specialization(db, specialization) }), @@ -3958,9 +3962,6 @@ impl<'db> Type<'db> { .map(|ty| ty.apply_specialization(db, specialization)), ), - // XXX: Is this right? - Type::PropertyInstance(_) => self, - Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy From 7aaeb47a4da346662bc0e6f4f4c171a530353582 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:42:51 -0400 Subject: [PATCH 064/100] Don't specialize function twice --- crates/red_knot_python_semantic/src/types.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 2625a278953da..f71cba81c9990 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4983,11 +4983,7 @@ impl<'db> FunctionType<'db> { /// /// This powers the `CallableTypeOf` special form from the `knot_extensions` module. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - let mut signature = self.signature(db).clone(); - if let Some(specialization) = self.specialization(db) { - signature.apply_specialization(db, specialization); - } - Type::Callable(CallableType::new(db, signature)) + Type::Callable(CallableType::new(db, self.signature(db).clone())) } /// Returns the [`FileRange`] of the function's name. From 43554e2ac9d7d7c0084bc34b56cb573370fecbdb Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 7 Apr 2025 18:49:36 -0400 Subject: [PATCH 065/100] fix docs --- crates/red_knot_python_semantic/src/types/class.rs | 8 ++++---- crates/red_knot_python_semantic/src/types/mro.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 3720ff8a2b9b5..25a5109611506 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -229,7 +229,7 @@ impl<'db> ClassType<'db> { /// /// If the MRO could not be accurately resolved, this method falls back to iterating /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`Class::try_mro`] if you need to distinguish between the success and failure + /// [`ClassLiteralType::try_mro`] if you need to distinguish between the success and failure /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order @@ -281,7 +281,7 @@ impl<'db> ClassType<'db> { /// or those marked as ClassVars are considered. /// /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope - /// directly. Use [`Class::class_member`] if you require a method that will + /// directly. Use [`ClassType::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { let (class_literal, _) = self.class_literal(db); @@ -499,7 +499,7 @@ impl<'db> ClassLiteralType<'db> { /// /// If the MRO could not be accurately resolved, this method falls back to iterating /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`Class::try_mro`] if you need to distinguish between the success and failure + /// [`ClassLiteralType::try_mro`] if you need to distinguish between the success and failure /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order @@ -744,7 +744,7 @@ impl<'db> ClassLiteralType<'db> { /// or those marked as ClassVars are considered. /// /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope - /// directly. Use [`Class::class_member`] if you require a method that will + /// directly. Use [`ClassLiteralType::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { let body_scope = self.body_scope(db); diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index 8f3edc3ca938c..f1d945cae8a8e 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -26,14 +26,14 @@ use crate::Db; /// class D[U](C[U]): ... /// ``` /// -/// See [`Class::iter_mro`] for more details. +/// See [`ClassType::iter_mro`] for more details. #[derive(PartialEq, Eq, Clone, Debug, salsa::Update)] pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>); impl<'db> Mro<'db> { /// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of /// base classes in the class definition, this operation is performed on a [class - /// literal][ClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a + /// literal][ClassLiteralType], not a [class type][ClassType]. (You can _also_ get the MRO of a /// class type, but this is done by first getting the MRO of the underlying class literal, and /// specializing each base class as needed if the class type is a generic alias.) /// @@ -220,7 +220,7 @@ impl<'db> FromIterator> for Mro<'db> { /// /// Even for first-party code, where we will have to resolve the MRO for every class we encounter, /// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the -/// Salsa-tracked [`Class::try_mro`] method unless it's absolutely necessary. +/// Salsa-tracked [`ClassLiteralType::try_mro`] method unless it's absolutely necessary. pub(super) struct MroIterator<'db> { db: &'db dyn Db, @@ -326,7 +326,7 @@ pub(super) enum MroErrorKind<'db> { /// The class has one or more duplicate bases. /// - /// This variant records the indices and [`Class`]es + /// This variant records the indices and [`ClassLiteralType`]s /// of the duplicate bases. The indices are the indices of nodes /// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node. /// Each index is the index of a node representing a duplicate base. From aec3392d29fdb1c16e91d226552daa1de01c481a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 8 Apr 2025 16:15:53 -0400 Subject: [PATCH 066/100] Add (optional) generic context to overload signature --- crates/red_knot_python_semantic/src/types.rs | 6 ++++- .../src/types/class.rs | 7 +++++ .../src/types/signatures.rs | 26 ++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f71cba81c9990..b34bc98eb4346 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3292,7 +3292,11 @@ impl<'db> Type<'db> { _ => { let signature = CallableSignature::single( self, - Signature::new(Parameters::gradual_form(), self.to_instance(db)), + Signature::new_generic( + class.generic_context(db), + Parameters::gradual_form(), + self.to_instance(db), + ), ); Signatures::single(signature) } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 25a5109611506..6cc08488eb75d 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -360,6 +360,13 @@ impl<'db> ClassLiteralType<'db> { self.class(db).known == Some(known_class) } + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + match self { + Self::NonGeneric(_) => None, + Self::Generic(generic) => Some(generic.generic_context(db)), + } + } + /// Return `true` if this class represents the builtin class `object` pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { self.is_known(db, KnownClass::Object) diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 6a1091398f88d..166371b4c9aae 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -14,7 +14,7 @@ use smallvec::{smallvec, SmallVec}; use super::{definition_expression_type, DynamicType, Type}; use crate::semantic_index::definition::Definition; -use crate::types::generics::Specialization; +use crate::types::generics::{GenericContext, Specialization}; use crate::types::todo_type; use crate::Db; use ruff_python_ast::{self as ast, name::Name}; @@ -162,6 +162,7 @@ impl<'db> CallableSignature<'db> { /// Return a signature for a dynamic callable pub(crate) fn dynamic(signature_type: Type<'db>) -> Self { let signature = Signature { + generic_context: None, parameters: Parameters::gradual_form(), return_ty: Some(signature_type), }; @@ -173,6 +174,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn todo(reason: &'static str) -> Self { let signature_type = todo_type!(reason); let signature = Signature { + generic_context: None, parameters: Parameters::todo(), return_ty: Some(signature_type), }; @@ -207,6 +209,9 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { /// The signature of one of the overloads of a callable. #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub struct Signature<'db> { + /// The generic context for this overload, if it is generic. + generic_context: Option>, + /// Parameters, in source order. /// /// The ordering of parameters in a valid signature must be: first positional-only parameters, @@ -224,6 +229,19 @@ pub struct Signature<'db> { impl<'db> Signature<'db> { pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option>) -> Self { Self { + generic_context: None, + parameters, + return_ty, + } + } + + pub(crate) fn new_generic( + generic_context: Option>, + parameters: Parameters<'db>, + return_ty: Option>, + ) -> Self { + Self { + generic_context, parameters, return_ty, } @@ -233,6 +251,7 @@ impl<'db> Signature<'db> { #[allow(unused_variables)] // 'reason' only unused in debug builds pub(crate) fn todo(reason: &'static str) -> Self { Signature { + generic_context: None, parameters: Parameters::todo(), return_ty: Some(todo_type!(reason)), } @@ -252,7 +271,11 @@ impl<'db> Signature<'db> { } }); + // TODO: Detect if a function definition is generic. For PEP 695 we can look at the + // function definition syntactically. For legacy typevars we'll have to infer the type of + // each parameter annotation. Self { + generic_context: None, parameters: Parameters::from_parameters( db, definition, @@ -280,6 +303,7 @@ impl<'db> Signature<'db> { pub(crate) fn bind_self(&self) -> Self { Self { + generic_context: self.generic_context, parameters: Parameters::new(self.parameters().iter().skip(1).cloned()), return_ty: self.return_ty, } From 595ecae0bb6e41f54a3a0213226cb529b675c9d8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 8 Apr 2025 16:53:18 -0400 Subject: [PATCH 067/100] Super-basic inference at call sites --- .../resources/mdtest/call/methods.md | 20 +-- .../resources/mdtest/generics/functions.md | 73 +++-------- .../resources/mdtest/generics/scoping.md | 10 +- crates/red_knot_python_semantic/src/types.rs | 12 +- .../src/types/builder.rs | 19 +-- .../src/types/call/bind.rs | 27 ++++- .../src/types/generics.rs | 114 +++++++++++++++++- .../src/types/infer.rs | 5 + .../src/types/signatures.rs | 8 +- 9 files changed, 191 insertions(+), 97 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 5aa3f0e3d27d9..f91452888d2c1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -410,29 +410,19 @@ def does_nothing[T](f: T) -> T: class C: @classmethod - # TODO: no error should be emitted here (needs support for generics) - # error: [invalid-argument-type] @does_nothing def f1(cls: type[C], x: int) -> str: return "a" - # TODO: no error should be emitted here (needs support for generics) - # error: [invalid-argument-type] + @does_nothing @classmethod def f2(cls: type[C], x: int) -> str: return "a" -# TODO: All of these should be `str` (and not emit an error), once we support generics - -# error: [call-non-callable] -reveal_type(C.f1(1)) # revealed: Unknown -# error: [call-non-callable] -reveal_type(C().f1(1)) # revealed: Unknown - -# error: [call-non-callable] -reveal_type(C.f2(1)) # revealed: Unknown -# error: [call-non-callable] -reveal_type(C().f2(1)) # revealed: Unknown +reveal_type(C.f1(1)) # revealed: str +reveal_type(C().f1(1)) # revealed: str +reveal_type(C.f2(1)) # revealed: str +reveal_type(C().f2(1)) # revealed: str ``` [functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index 32b4f1971b052..667b0e5641029 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -51,25 +51,10 @@ the inferred type to e.g. `int`. def f[T](x: T) -> T: return x -# TODO: no error -# TODO: revealed: int or Literal[1] -# error: [invalid-argument-type] -reveal_type(f(1)) # revealed: T - -# TODO: no error -# TODO: revealed: float -# error: [invalid-argument-type] -reveal_type(f(1.0)) # revealed: T - -# TODO: no error -# TODO: revealed: bool or Literal[true] -# error: [invalid-argument-type] -reveal_type(f(True)) # revealed: T - -# TODO: no error -# TODO: revealed: str or Literal["string"] -# error: [invalid-argument-type] -reveal_type(f("string")) # revealed: T +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(1.0)) # revealed: float +reveal_type(f(True)) # revealed: Literal[True] +reveal_type(f("string")) # revealed: Literal["string"] ``` ## Inferring “deep” generic parameter types @@ -82,7 +67,7 @@ def f[T](x: list[T]) -> T: return x[0] # TODO: revealed: float -reveal_type(f([1.0, 2.0])) # revealed: T +reveal_type(f([1.0, 2.0])) # revealed: Unknown ``` ## Typevar constraints @@ -162,61 +147,39 @@ parameters simultaneously. def two_params[T](x: T, y: T) -> T: return x -# TODO: no error # TODO: revealed: str -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(two_params("a", "b")) # revealed: T +reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] -# TODO: no error # TODO: revealed: str | int -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(two_params("a", 1)) # revealed: T +reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] ``` ```py def param_with_union[T](x: T | int, y: T) -> T: return y -# TODO: no error # TODO: revealed: str -# error: [invalid-argument-type] -reveal_type(param_with_union(1, "a")) # revealed: T +reveal_type(param_with_union(1, "a")) # revealed: Literal["a"] -# TODO: no error # TODO: revealed: str -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(param_with_union("a", "a")) # revealed: T +reveal_type(param_with_union("a", "a")) # revealed: Literal["a"] -# TODO: no error # TODO: revealed: int -# error: [invalid-argument-type] -reveal_type(param_with_union(1, 1)) # revealed: T +reveal_type(param_with_union(1, 1)) # revealed: Literal[1] -# TODO: no error # TODO: revealed: str | int -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(param_with_union("a", 1)) # revealed: T +reveal_type(param_with_union("a", 1)) # revealed: Literal["a", 1] ``` ```py def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]: return y -# TODO: no error # TODO: revealed: tuple[str, int] -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[T, S] +reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] -# TODO: no error # TODO: revealed: tuple[str, int] -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[T, S] +reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] ``` ## Inferring nested generic function calls @@ -231,15 +194,9 @@ def f[T](x: T) -> tuple[T, int]: def g[T](x: T) -> T | None: return x -# TODO: no error # TODO: revealed: tuple[str | None, int] -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(f(g("a"))) # revealed: tuple[T, int] +reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] -# TODO: no error # TODO: revealed: tuple[str, int] | None -# error: [invalid-argument-type] -# error: [invalid-argument-type] -reveal_type(g(f("a"))) # revealed: T | None +reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index bb7660f3241c5..7ab6dda49083e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -59,14 +59,8 @@ to a different type each time. def f[T](x: T) -> T: return x -# TODO: no error -# TODO: revealed: int or Literal[1] -# error: [invalid-argument-type] -reveal_type(f(1)) # revealed: T -# TODO: no error -# TODO: revealed: str or Literal["a"] -# error: [invalid-argument-type] -reveal_type(f("a")) # revealed: T +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f("a")) # revealed: Literal["a"] ``` ## Methods can mention class typevars diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b34bc98eb4346..f9e4bd9ec09cd 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -35,7 +35,7 @@ use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; use crate::types::call::{Bindings, CallArgumentTypes}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; -use crate::types::generics::Specialization; +use crate::types::generics::{GenericContext, Specialization}; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; @@ -4334,6 +4334,10 @@ impl<'db> TypeVarInstance<'db> { None } } + + pub(crate) fn default_type(self, db: &'db dyn Db) -> Type<'db> { + self.default_ty(db).unwrap_or(Type::unknown()) + } } #[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)] @@ -4971,6 +4975,9 @@ pub struct FunctionType<'db> { /// A set of special decorators that were applied to this function decorators: FunctionDecorators, + /// The generic context of a generic function. + generic_context: Option>, + /// A specialization that should be applied to the function's parameter and return types, /// either because the function is itself generic, or because it appears in the body of a /// generic class. @@ -5052,7 +5059,7 @@ impl<'db> FunctionType<'db> { let scope = self.body_scope(db); let function_stmt_node = scope.node(db).expect_function(); let definition = self.definition(db); - Signature::from_function(db, definition, function_stmt_node) + Signature::from_function(db, self.generic_context(db), definition, function_stmt_node) } pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { @@ -5070,6 +5077,7 @@ impl<'db> FunctionType<'db> { self.known(db), self.body_scope(db), self.decorators(db), + None, // If the function was generic before, it isn't anymore Some(specialization), ) } diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index c932474703500..915a88bfe707c 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -48,27 +48,32 @@ impl<'db> UnionBuilder<'db> { } /// Collapse the union to a single type: `object`. - fn collapse_to_object(mut self) -> Self { + fn collapse_to_object(&mut self) { self.elements.clear(); self.elements.push(Type::object(self.db)); - self } /// Adds a type to this union. pub(crate) fn add(mut self, ty: Type<'db>) -> Self { + self.add_in_place(ty); + self + } + + /// Adds a type to this union. + pub(crate) fn add_in_place(&mut self, ty: Type<'db>) { match ty { Type::Union(union) => { let new_elements = union.elements(self.db); self.elements.reserve(new_elements.len()); for element in new_elements { - self = self.add(*element); + self.add_in_place(*element); } } // Adding `Never` to a union is a no-op. Type::Never => {} // Adding `object` to a union results in `object`. ty if ty.is_object(self.db) => { - return self.collapse_to_object(); + self.collapse_to_object(); } _ => { let bool_pair = if let Type::BooleanLiteral(b) = ty { @@ -96,7 +101,7 @@ impl<'db> UnionBuilder<'db> { || ty.is_subtype_of(self.db, *element) || element.is_object(self.db) { - return self; + return; } else if element.is_subtype_of(self.db, ty) { to_remove.push(index); } else if ty_negated.is_subtype_of(self.db, *element) { @@ -107,7 +112,8 @@ impl<'db> UnionBuilder<'db> { // `element | ty` must be `object` (object has no other supertypes). This means we can simplify // the whole union to just `object`, since all other potential elements would also be subtypes of // `object`. - return self.collapse_to_object(); + self.collapse_to_object(); + return; } } if let Some((&first, rest)) = to_remove.split_first() { @@ -121,7 +127,6 @@ impl<'db> UnionBuilder<'db> { } } } - self } pub(crate) fn build(self) -> Type<'db> { diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 1c823845f5633..ee1b13f8f3dfd 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -16,6 +16,7 @@ use crate::types::diagnostic::{ NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; +use crate::types::generics::SpecializationBuilder; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ todo_type, BoundMethodType, FunctionDecorators, KnownClass, KnownFunction, KnownInstanceType, @@ -964,6 +965,23 @@ impl<'db> Binding<'db> { argument_types: &CallArgumentTypes<'_, 'db>, ) { let parameters = signature.parameters(); + let specialization = signature.generic_context.map(|generic_context| { + let mut builder = SpecializationBuilder::new(db, generic_context); + for (argument_index, (_, argument_type)) in argument_types.iter().enumerate() { + let Some(parameter_index) = self.argument_parameters[argument_index] else { + // There was an error with argument when matching parameters, so don't bother + // type-checking it. + continue; + }; + let parameter = ¶meters[parameter_index]; + let Some(expected_type) = parameter.annotated_type() else { + continue; + }; + builder.infer(expected_type, argument_type); + } + builder.build() + }); + let mut num_synthetic_args = 0; let get_argument_index = |argument_index: usize, num_synthetic_args: usize| { if argument_index >= num_synthetic_args { @@ -986,7 +1004,10 @@ impl<'db> Binding<'db> { continue; }; let parameter = ¶meters[parameter_index]; - if let Some(expected_ty) = parameter.annotated_type() { + if let Some(mut expected_ty) = parameter.annotated_type() { + if let Some(specialization) = specialization { + expected_ty = expected_ty.apply_specialization(db, specialization); + } if !argument_type.is_assignable_to(db, expected_ty) { let positional = matches!(argument, Argument::Positional | Argument::Synthetic) && !parameter.is_variadic(); @@ -1008,6 +1029,10 @@ impl<'db> Binding<'db> { self.parameter_tys[parameter_index] = Some(union); } } + + if let Some(specialization) = specialization { + self.return_ty = self.return_ty.apply_specialization(db, specialization); + } } pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 946b38f75b510..33fd77089fd52 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -1,10 +1,11 @@ use ruff_python_ast as ast; +use rustc_hash::FxHashMap; use crate::semantic_index::SemanticIndex; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance, - UnionType, + UnionBuilder, UnionType, }; use crate::Db; @@ -141,3 +142,114 @@ impl<'db> Specialization<'db> { .map(|(_, ty)| *ty) } } + +/// Performs type inference between parameter annotations and argument types, producing a +/// specialization of a generic function. +pub(crate) struct SpecializationBuilder<'db> { + db: &'db dyn Db, + generic_context: GenericContext<'db>, + types: FxHashMap, UnionBuilder<'db>>, +} + +impl<'db> SpecializationBuilder<'db> { + pub(crate) fn new(db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { + Self { + db, + generic_context, + types: FxHashMap::default(), + } + } + + pub(crate) fn build(mut self) -> Specialization<'db> { + let types = self + .generic_context + .variables(self.db) + .iter() + .map(|variable| { + self.types + .remove(variable) + .map(UnionBuilder::build) + .unwrap_or(variable.default_type(self.db)) + }) + .collect(); + Specialization::new(self.db, self.generic_context, types) + } + + fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) { + let builder = self + .types + .entry(typevar) + .or_insert_with(|| UnionBuilder::new(self.db)); + builder.add_in_place(ty); + } + + pub(crate) fn infer(&mut self, formal: Type<'db>, actual: Type<'db>) { + // If the actual type is already assignable to the formal type, then return without adding + // any new type mappings. (Note that if the formal type contains any typevars, this check + // will fail, since no non-typevar types are assignable to a typevar.) + // + // In particular, this handles a case like + // + // ```py + // def f[T](t: T | None): ... + // + // f(None) + // ``` + // + // without specializing `T` to `None`. + if actual.is_assignable_to(self.db, formal) { + return; + } + + match (formal, actual) { + (Type::TypeVar(typevar), _) => self.add_type_mapping(typevar, actual), + + (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { + let formal_elements = formal_tuple.elements(self.db); + let actual_elements = actual_tuple.elements(self.db); + if formal_elements.len() == actual_elements.len() { + for (formal_element, actual_element) in + formal_elements.iter().zip(actual_elements) + { + self.infer(*formal_element, *actual_element); + } + } + } + + (Type::Union(formal), _) => { + // TODO: We haven't implemented a full unification solver yet. If typevars appear + // in multiple union elements, we ideally want to express that _only one_ of them + // needs to match, and that we should infer the smallest type mapping that allows + // that. + // + // For now, we punt on handling multiple typevar elements. Instead, if _precisely + // one_ union element _is_ a typevar (not _contains_ a typevar), then we go ahead + // and add a mapping between that typevar and the actual type. (Note that we've + // already handled above the case where the actual is assignable to a _non-typevar_ + // union element.) + let mut typevars = formal.iter(self.db).filter_map(|ty| match ty { + Type::TypeVar(typevar) => Some(*typevar), + _ => None, + }); + let typevar = typevars.next(); + let additional_typevars = typevars.next(); + if let (Some(typevar), None) = (typevar, additional_typevars) { + self.add_type_mapping(typevar, actual); + } + } + + (Type::Intersection(formal), _) => { + // The actual type must be assignable to every (positive) element of the + // formal intersection, so we must infer type mappings for each of them. (The + // actual type must also be disjoint from every negative element of the + // intersection, but that doesn't help us infer any type mappings.) + for positive in formal.iter_positive(self.db) { + self.infer(positive, actual); + } + } + + // TODO: Add more forms that we can structurally induct into: type[C], callables + _ => {} + } + } +} diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e73017e04c2e5..3fd33e6ee46ec 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1444,6 +1444,10 @@ impl<'db> TypeInferenceBuilder<'db> { } } + let generic_context = type_params.as_ref().map(|type_params| { + GenericContext::from_type_params(self.db(), self.index, type_params) + }); + let function_kind = KnownFunction::try_from_definition_and_name(self.db(), definition, name); @@ -1460,6 +1464,7 @@ impl<'db> TypeInferenceBuilder<'db> { function_kind, body_scope, function_decorators, + generic_context, specialization, )); diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 166371b4c9aae..7f89c24a896bb 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -210,7 +210,7 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub struct Signature<'db> { /// The generic context for this overload, if it is generic. - generic_context: Option>, + pub(crate) generic_context: Option>, /// Parameters, in source order. /// @@ -260,6 +260,7 @@ impl<'db> Signature<'db> { /// Return a typed signature from a function definition. pub(super) fn from_function( db: &'db dyn Db, + generic_context: Option>, definition: Definition<'db>, function_node: &ast::StmtFunctionDef, ) -> Self { @@ -271,11 +272,8 @@ impl<'db> Signature<'db> { } }); - // TODO: Detect if a function definition is generic. For PEP 695 we can look at the - // function definition syntactically. For legacy typevars we'll have to infer the type of - // each parameter annotation. Self { - generic_context: None, + generic_context, parameters: Parameters::from_parameters( db, definition, From 58830faf988aeeee07d8cbbc4c0d941b0176bd7e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 8 Apr 2025 17:05:22 -0400 Subject: [PATCH 068/100] Add comment --- crates/red_knot_python_semantic/src/types/call/bind.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index ee1b13f8f3dfd..dce041fb8d860 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -964,6 +964,8 @@ impl<'db> Binding<'db> { signature: &Signature<'db>, argument_types: &CallArgumentTypes<'_, 'db>, ) { + // If this overload is generic, first see if we can infer a specialization of the function + // from the arguments that were passed in. let parameters = signature.parameters(); let specialization = signature.generic_context.map(|generic_context| { let mut builder = SpecializationBuilder::new(db, generic_context); From 7c3405adb19e66ee66b1a32809437cc3bae5bd4d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 09:52:06 -0400 Subject: [PATCH 069/100] Better TODO fallback type --- .../resources/mdtest/generics/classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9075f29518a25..e5d44aefd529a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -101,7 +101,7 @@ class Constrained[T: (int, str)]: ... reveal_type(Constrained[int]()) # revealed: Constrained[int] # TODO: error: [invalid-argument-type] -# TODO: revealed: Unknown +# TODO: revealed: Constrained[Unknown] reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] reveal_type(Constrained[str]()) # revealed: Constrained[str] From 6593d90dbd63af944f4d12bde0fb7b0a2ffb2ea8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 09:53:26 -0400 Subject: [PATCH 070/100] Generic aliases are literals in type display --- crates/red_knot_python_semantic/resources/mdtest/stubs/class.md | 2 +- crates/red_knot_python_semantic/src/types/display.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md index 2716eb0a98837..233fa3f6f28c1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md @@ -11,7 +11,7 @@ class Foo[T]: ... class Bar(Foo[Bar]): ... reveal_type(Bar) # revealed: Literal[Bar] -reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Foo[Bar], Literal[object]] +reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo[Bar]], Literal[object]] ``` ## Access to attributes declared in stubs diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 96083462bf1ff..36832f8a8af38 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -44,6 +44,7 @@ impl Display for DisplayType<'_> { | Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::ClassLiteral(_) + | Type::GenericAlias(_) | Type::FunctionLiteral(_) => { write!(f, "Literal[{representation}]") } From 7ca6a60b16b0e558f95afc4c13d617b5333c65fe Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 10:00:27 -0400 Subject: [PATCH 071/100] Explain self_instance not being specialized --- crates/red_knot_python_semantic/src/types.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 421aeea7746ff..99ee1f5edf21e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4161,6 +4161,14 @@ impl<'db> Type<'db> { Type::FunctionLiteral(function.apply_specialization(db, specialization)) } + // Note that we don't need to apply the specialization to `self_instance`, since it + // must either be a non-generic class literal (which cannot have any typevars to + // specialize) or a generic alias (which has already been fully specialized). For a + // generic alias, the specialization being applied here must be for some _other_ + // generic context nested within the generic alias's class literal, which the generic + // alias's context cannot refer to. (The _method_ does need to be specialized, since it + // might be a nested generic method, whose generic context is what is now being + // specialized.) Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( db, method.function(db).apply_specialization(db, specialization), From dea493efb78bfaa0e292ab40a578f9ccfe144afb Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 10:04:09 -0400 Subject: [PATCH 072/100] Comment other non-specializations --- crates/red_knot_python_semantic/src/types.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 99ee1f5edf21e..c9f4a58f4020e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4236,7 +4236,12 @@ impl<'db> Type<'db> { | Type::AlwaysFalsy | Type::WrapperDescriptor(_) | 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(_) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -4244,6 +4249,8 @@ impl<'db> Type<'db> { | Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::SliceLiteral(_) + // Instance contains a ClassType, which has already been specialized if needed, like + // above with BoundMethod's self_instance. | Type::Instance(_) | Type::KnownInstance(_) => self, } From 048bb8be16d02acb8466a7bcceffa983ae959f39 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 10:09:43 -0400 Subject: [PATCH 073/100] Add xfail for generic method inside generic class --- .../resources/mdtest/generics/classes.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 e5d44aefd529a..a73c57ca10183 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -183,6 +183,30 @@ reveal_type(Base[int].x) # revealed: int | None reveal_type(Sub[int].x) # revealed: int | None ``` +## Generic methods + +Generic classes can contain methods that are themselves generic. The generic methods can refer to +the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in +scope for the method. + +```py +class C[T]: + def method[U](self, u: U) -> U: + return u + + # error: [unresolved-reference] + def cannot_use_outside_of_method(self, u: U): ... + + # TODO: error + def cannot_shadow_class_typevar[T](self, t: T): ... + +c: C[int] = C[int]() +# TODO: no error +# TODO: revealed: str or Literal["string"] +# error: [invalid-argument-type] +reveal_type(c.method("string")) # revealed: U +``` + ## Cyclic class definition A class can use itself as the type parameter of one of its superclasses. (This is also known as the From 06859fa871d7789d446afee7868f67c9e4b1a743 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 10:23:28 -0400 Subject: [PATCH 074/100] More Python-like displays for specializations --- .../resources/mdtest/generics/scoping.md | 16 ++-- .../src/types/display.rs | 92 ++++++++++++------- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index bb7660f3241c5..2884b8d1b1b41 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -102,18 +102,18 @@ class C[T]: def f(self, x: T) -> str: return "a" -reveal_type(getattr_static(C[int], "f")) # revealed: Literal[] -reveal_type(getattr_static(C[int], "f").__get__) # revealed: -reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[] -# revealed: +reveal_type(getattr_static(C[int], "f")) # revealed: Literal[f[int]] +reveal_type(getattr_static(C[int], "f").__get__) # revealed: +reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[f[int]] +# revealed: reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int])) -reveal_type(C[int].f) # revealed: Literal[] -reveal_type(C[int]().f) # revealed: +reveal_type(C[int].f) # revealed: Literal[f[int]] +reveal_type(C[int]().f) # revealed: bound_method = C[int]().f reveal_type(bound_method.__self__) # revealed: C[int] -reveal_type(bound_method.__func__) # revealed: Literal[] +reveal_type(bound_method.__func__) # revealed: Literal[f[int]] reveal_type(C[int]().f(1)) # revealed: str reveal_type(bound_method(1)) # revealed: str @@ -124,7 +124,7 @@ reveal_type(C[int].f(C[int](), 1)) # revealed: str class D[U](C[U]): pass -reveal_type(D[int]().f) # revealed: +reveal_type(D[int]().f) # revealed: ``` ## Methods can mention other typevars diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 36832f8a8af38..87825bc5a684c 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -95,41 +95,45 @@ impl Display for DisplayRepresentation<'_> { }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => { + f.write_str(function.name(self.db))?; if let Some(specialization) = function.specialization(self.db) { - write!( - f, - "<{name} specialized with {specialization}>", - name = function.name(self.db), - specialization = specialization.display(self.db), - ) - } else { - f.write_str(function.name(self.db)) + specialization.display_short(self.db).fmt(f)?; } + Ok(()) } Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f), Type::BoundMethod(bound_method) => { let function = bound_method.function(self.db); + let self_instance = bound_method.self_instance(self.db); + let self_instance_specialization = match self_instance { + Type::Instance(InstanceType { + class: ClassType::Generic(alias), + }) => Some(alias.specialization(self.db)), + _ => None, + }; + let specialization = match function.specialization(self.db) { + Some(specialization) + if self_instance_specialization.is_none_or(|sis| specialization == sis) => + { + specialization.display_short(self.db).to_string() + } + _ => String::new(), + }; write!( f, - "", + "", method = function.name(self.db), instance = bound_method.self_instance(self.db).display(self.db), - specialization = if let Some(specialization) = function.specialization(self.db) - { - format!(" specialized with {}", specialization.display(self.db)) - } else { - String::new() - }, ) } Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { write!( f, - "", + "", function = function.name(self.db), specialization = if let Some(specialization) = function.specialization(self.db) { - format!(" specialized with {}", specialization.display(self.db)) + specialization.display_short(self.db).to_string() } else { String::new() }, @@ -206,7 +210,7 @@ impl<'db> GenericAlias<'db> { pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { DisplayGenericAlias { origin: self.origin(db), - types: self.specialization(db).types(db), + specialization: self.specialization(db), db, } } @@ -214,29 +218,39 @@ impl<'db> GenericAlias<'db> { pub(crate) struct DisplayGenericAlias<'db> { origin: GenericClass<'db>, - types: &'db [Type<'db>], + specialization: Specialization<'db>, db: &'db dyn Db, } impl Display for DisplayGenericAlias<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{origin}[", origin = self.origin.class(self.db).name,)?; - for (idx, ty) in self.types.iter().enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - write!(f, "{}", ty.display(self.db))?; - } - f.write_str("]") + write!( + f, + "{origin}{specialization}", + origin = self.origin.class(self.db).name, + specialization = self.specialization.display_short(self.db), + ) } } impl<'db> Specialization<'db> { + /// Renders the specialization in full, e.g. `{T = int, U = str}`. pub fn display(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { DisplaySpecialization { typevars: self.generic_context(db).variables(db), types: self.types(db), db, + full: true, + } + } + + /// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`. + pub fn display_short(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + DisplaySpecialization { + typevars: self.generic_context(db).variables(db), + types: self.types(db), + db, + full: false, } } } @@ -245,18 +259,30 @@ pub struct DisplaySpecialization<'db> { typevars: &'db [TypeVarInstance<'db>], types: &'db [Type<'db>], db: &'db dyn Db, + full: bool, } impl Display for DisplaySpecialization<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_char('{')?; - for (idx, (var, ty)) in self.typevars.iter().zip(self.types).enumerate() { - if idx > 0 { - f.write_str(", ")?; + if self.full { + f.write_char('{')?; + for (idx, (var, ty)) in self.typevars.iter().zip(self.types).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{} = {}", var.name(self.db), ty.display(self.db))?; + } + f.write_char('}') + } else { + f.write_char('[')?; + for (idx, (_, ty)) in self.typevars.iter().zip(self.types).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{}", ty.display(self.db))?; } - write!(f, "{} = {}", var.name(self.db), ty.display(self.db))?; + f.write_char(']') } - f.write_char('}') } } From d1384050da59525f7f43d5bad7074d416a90c490 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 11:04:46 -0400 Subject: [PATCH 075/100] Narrow type(generic) better --- .../resources/mdtest/narrow/type.md | 18 ++++++++++++++++++ .../src/types/class.rs | 13 +++++++++++++ .../src/types/generics.rs | 5 +++++ .../src/types/narrow.rs | 16 +++++++++++----- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index 0b7f0f49b6d20..e77a6152e7717 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -109,6 +109,24 @@ def _(x: A | B): reveal_type(x) # revealed: A ``` +## Narrowing for generic classes + +Note that `type` returns the runtime class of an object, which does _not_ include specializations in +the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the +specialization that we compare with; we must narrow to an unknown specialization of the generic +class. + +```py +class A[T = int]: ... +class B: ... + +def _[T](x: A | B): + if type(x) is A[str]: + reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown] + else: + reveal_type(x) # revealed: A[int] | B +``` + ## Limitations ```py diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 805f229b28e4a..a3fe1024b40cc 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -408,6 +408,19 @@ impl<'db> ClassLiteralType<'db> { } } + /// Returns the unknown specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// maps each of the class's typevars to `Unknown`. + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + match self { + Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), + Self::Generic(generic) => { + let specialization = generic.generic_context(db).unknown_specialization(db); + ClassType::Generic(GenericAlias::new(db, generic, specialization)) + } + } + } + /// Return an iterator over the inferred types of this class's *explicit* bases. /// /// Note that any class (except for `object`) that has no explicit diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 946b38f75b510..9191f946f6dd2 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -85,6 +85,11 @@ impl<'db> GenericContext<'db> { self.specialize(db, types) } + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + let types = vec![Type::unknown(); self.variables(db).len()]; + self.specialize(db, types.into()) + } + pub(crate) fn specialize( self, db: &'db dyn Db, diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 32d5f213ff931..ba55b31c201c9 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -8,8 +8,8 @@ use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; use crate::semantic_index::symbol_table; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - infer_expression_types, IntersectionBuilder, KnownClass, SubclassOfType, Truthiness, Type, - UnionBuilder, + infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, SubclassOfType, + Truthiness, Type, UnionBuilder, }; use crate::Db; use itertools::Itertools; @@ -470,8 +470,14 @@ impl<'db> NarrowingConstraintsBuilder<'db> { range: _, }, }) if keywords.is_empty() => { - let Type::ClassLiteral(rhs_class) = rhs_ty else { - continue; + let rhs_class = match rhs_ty { + Type::ClassLiteral(class) => class, + Type::GenericAlias(alias) => { + ClassLiteralType::Generic(alias.origin(self.db)) + } + _ => { + continue; + } }; let [ast::Expr::Name(ast::ExprName { id, .. })] = &**args else { @@ -498,7 +504,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let symbol = self.expect_expr_name_symbol(id); constraints.insert( symbol, - Type::instance(rhs_class.default_specialization(self.db)), + Type::instance(rhs_class.unknown_specialization(self.db)), ); } } From 0997eca7fa6ea892f938deb4b0f27e5b29d20fc2 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 11:04:58 -0400 Subject: [PATCH 076/100] Better todos --- crates/red_knot_python_semantic/src/types/generics.rs | 2 +- crates/red_knot_python_semantic/src/types/infer.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 9191f946f6dd2..977808c84b54e 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -67,7 +67,7 @@ impl<'db> GenericContext<'db> { } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { // TODO: This should be a new type variant where only these exact types are - // assignable, and not subclasses of them. + // assignable, and not subclasses of them, nor a union of them. parameter = parameter .with_annotated_type(UnionType::from_elements(db, constraints.iter(db))); } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 2c8aa6e47d2ea..bee37ffefa1c4 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1734,7 +1734,9 @@ impl<'db> TypeInferenceBuilder<'db> { } // Inference of bases deferred in stubs - // TODO also defer stringified generic type parameters + // TODO: Only defer the references that are actually string literals, instead of + // deferring the entire class definition if a string literal occurs anywhere in the + // base class list. if self.are_all_types_deferred() || class_node.bases().iter().any(contains_string_literal) { From 6ab1b9060a4f6c350b7659c46be3bc0ce5f740ca Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 11:07:00 -0400 Subject: [PATCH 077/100] Add TODO about property test data --- .../src/types/property_tests/type_generation.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs index 1fe881fbb385f..807219015961a 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs @@ -11,6 +11,8 @@ use ruff_python_ast::name::Name; /// A test representation of a type that can be transformed unambiguously into a real Type, /// given a db. +/// +/// TODO: We should add some variants that exercise generic classes and specializations thereof. #[derive(Debug, Clone, PartialEq)] pub(crate) enum Ty { Never, From bcb147ffa8de1bf81086a953a4ea1e6e1e6d5756 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 11:09:20 -0400 Subject: [PATCH 078/100] lint --- .../resources/mdtest/generics/classes.md | 3 +-- 1 file changed, 1 insertion(+), 2 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 a73c57ca10183..8630731764a0c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -193,7 +193,6 @@ scope for the method. class C[T]: def method[U](self, u: U) -> U: return u - # error: [unresolved-reference] def cannot_use_outside_of_method(self, u: U): ... @@ -204,7 +203,7 @@ c: C[int] = C[int]() # TODO: no error # TODO: revealed: str or Literal["string"] # error: [invalid-argument-type] -reveal_type(c.method("string")) # revealed: U +reveal_type(c.method("string")) # revealed: U ``` ## Cyclic class definition From 3a7b638654280f9c1266869283323dcd2adfcc81 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 12:37:16 -0400 Subject: [PATCH 079/100] Clean up union examples --- .../resources/mdtest/generics/functions.md | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index 667b0e5641029..20bb1643120f4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -154,21 +154,28 @@ reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] ``` -```py -def param_with_union[T](x: T | int, y: T) -> T: - return y +When one of the parameters is a union, we attempt to find the smallest specialization that satisfies +all of the constraints. -# TODO: revealed: str -reveal_type(param_with_union(1, "a")) # revealed: Literal["a"] +```py +# TODO: make this return list[T], so that we can write a correct body +# error: [invalid-return-type] +def union_param[T](x: T | None) -> T: ... -# TODO: revealed: str -reveal_type(param_with_union("a", "a")) # revealed: Literal["a"] +reveal_type(union_param("a")) # revealed: Literal["a"] +reveal_type(union_param(1)) # revealed: Literal[1] +reveal_type(union_param(None)) # revealed: Unknown +``` -# TODO: revealed: int -reveal_type(param_with_union(1, 1)) # revealed: Literal[1] +```py +def union_and_nonunion_params[T](x: T | int, y: T) -> T: + return y -# TODO: revealed: str | int -reveal_type(param_with_union("a", 1)) # revealed: Literal["a", 1] +reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] ``` ```py From cccc77de503c9b93f024590a29df722543fd0bfb Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 14:26:53 -0400 Subject: [PATCH 080/100] Use with_self instead of prepend_synthetic --- crates/red_knot_python_semantic/src/types.rs | 14 ++++++-------- .../src/types/call/arguments.rs | 15 --------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index be76aad94ca07..d670346804bd0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3710,7 +3710,7 @@ impl<'db> Type<'db> { fn try_call_constructor( self, db: &'db dyn Db, - argument_types: CallArgumentTypes<'_, 'db>, + mut argument_types: CallArgumentTypes<'_, 'db>, ) -> Result, ConstructorCallError<'db>> { debug_assert!(matches!(self, Type::ClassLiteral(_) | Type::SubclassOf(_))); @@ -3749,11 +3749,9 @@ impl<'db> Type<'db> { Symbol::Type(dunder_callable, boundness) => { let signatures = dunder_callable.signatures(db); // `__new__` is a static method, so we must inject the `cls` argument. - let mut argument_types = argument_types.prepend_synthetic(self); - - Some( - match Bindings::match_parameters(signatures, &mut argument_types) - .check_types(db, &mut argument_types) + Some(argument_types.with_self(Some(self), |argument_types| { + match Bindings::match_parameters(signatures, argument_types) + .check_types(db, argument_types) { Ok(bindings) => { if boundness == Boundness::PossiblyUnbound { @@ -3763,8 +3761,8 @@ impl<'db> Type<'db> { } } Err(err) => Err(err.into()), - }, - ) + } + })) } // No explicit `__new__` method found Symbol::Unbound => None, diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index fd2cd4a3131d3..cce0c81c0ba37 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -109,21 +109,6 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { result } - /// Create a new [`CallArgumentTypes`] by prepending a synthetic argument to the front of this - /// argument list. - pub(crate) fn prepend_synthetic(&self, synthetic: Type<'db>) -> Self { - Self { - arguments: CallArguments( - std::iter::once(Argument::Synthetic) - .chain(self.arguments.iter()) - .collect(), - ), - types: std::iter::once(synthetic) - .chain(self.types.iter().copied()) - .collect(), - } - } - pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { self.arguments.iter().zip(self.types.iter().copied()) } From 993c6e3444a7ab4fc6606ed00230084ed482ee12 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 16:14:48 -0400 Subject: [PATCH 081/100] Use try_call_dunder_with_policy --- crates/red_knot_python_semantic/src/types.rs | 57 +++++++------------ .../src/types/infer.rs | 5 +- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d670346804bd0..4f271cf1d676f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3512,9 +3512,14 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, name: &str, - argument_types: CallArgumentTypes<'_, 'db>, + mut argument_types: CallArgumentTypes<'_, 'db>, ) -> Result, CallDunderError<'db>> { - self.try_call_dunder_with_policy(db, name, argument_types, MemberLookupPolicy::empty()) + self.try_call_dunder_with_policy( + db, + name, + &mut argument_types, + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) } /// Same as `try_call_dunder`, but allows specifying a policy for the member lookup. In @@ -3525,21 +3530,17 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, name: &str, - mut argument_types: CallArgumentTypes<'_, 'db>, + argument_types: &mut CallArgumentTypes<'_, 'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { match self - .member_lookup_with_policy( - db, - name.into(), - MemberLookupPolicy::NO_INSTANCE_FALLBACK | policy, - ) + .member_lookup_with_policy(db, name.into(), policy) .symbol { Symbol::Type(dunder_callable, boundness) => { let signatures = dunder_callable.signatures(db); - let bindings = Bindings::match_parameters(signatures, &mut argument_types) - .check_types(db, &mut argument_types)?; + let bindings = Bindings::match_parameters(signatures, argument_types) + .check_types(db, argument_types)?; if boundness == Boundness::PossiblyUnbound { return Err(CallDunderError::PossiblyUnbound(Box::new(bindings))); } @@ -3737,36 +3738,20 @@ impl<'db> Type<'db> { // `object` we would inadvertently unhide `__new__` on `type`, which is not what we want. // An alternative might be to not skip `object.__new__` but instead mark it such that it's // easy to check if that's the one we found? - let new_call_outcome: Option, CallDunderError<'db>>> = match self - .member_lookup_with_policy( + // Note that `__new__` is a static method, so we must inject the `cls` argument. + let new_call_outcome = argument_types.with_self(Some(self), |argument_types| { + let result = self.try_call_dunder_with_policy( db, - "__new__".into(), + "__new__", + argument_types, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ) - .symbol - { - Symbol::Type(dunder_callable, boundness) => { - let signatures = dunder_callable.signatures(db); - // `__new__` is a static method, so we must inject the `cls` argument. - Some(argument_types.with_self(Some(self), |argument_types| { - match Bindings::match_parameters(signatures, argument_types) - .check_types(db, argument_types) - { - Ok(bindings) => { - if boundness == Boundness::PossiblyUnbound { - Err(CallDunderError::PossiblyUnbound(Box::new(bindings))) - } else { - Ok(bindings) - } - } - Err(err) => Err(err.into()), - } - })) + ); + match result { + Err(CallDunderError::MethodNotAvailable) => None, + _ => Some(result), } - // No explicit `__new__` method found - Symbol::Unbound => None, - }; + }); // TODO: we should use the actual return type of `__new__` to determine the instance type let instance_ty = self diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 4b7a6da8c0153..3d7bf38616f15 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2509,14 +2509,15 @@ impl<'db> TypeInferenceBuilder<'db> { let result = object_ty.try_call_dunder_with_policy( db, "__setattr__", - CallArgumentTypes::positional([ + &mut CallArgumentTypes::positional([ Type::StringLiteral(StringLiteralType::new( db, Box::from(attribute), )), value_ty, ]), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); match result { From 2169282fc2061d7ef1c2053ab456d785b4f06533 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 16:31:52 -0400 Subject: [PATCH 082/100] Try to infer class specialization from constructor arguments --- .../resources/mdtest/generics/classes.md | 12 ++- crates/red_knot_python_semantic/src/types.rs | 73 +++++++++++++++++-- .../src/types/call.rs | 2 +- .../src/types/call/bind.rs | 24 +++++- .../src/types/generics.rs | 5 ++ .../src/types/infer.rs | 36 ++++----- .../src/types/signatures.rs | 16 ++++ 7 files changed, 138 insertions(+), 30 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 8630731764a0c..8144e1ac459a5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -157,7 +157,7 @@ class E[T]: def __init__(self, x: T) -> None: ... # TODO: revealed: E[int] or E[Literal[1]] -reveal_type(E(1)) # revealed: E[Unknown] +reveal_type(E(1)) # revealed: E[int] ``` The types inferred from a type context and from a constructor parameter must be consistent with each @@ -168,6 +168,16 @@ other: wrong_innards: E[int] = E("five") ``` +## fwomp + +```py +class E[T]: + def __init__(self, x: T) -> None: ... + +# TODO: revealed: E[int] or E[Literal[1]] +reveal_type(E(1)) # revealed: E[int] +``` + ## Generic subclass When a generic subclass fills its superclass's type parameter with one of its own, the actual types diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4f271cf1d676f..b2a151a48e343 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -32,7 +32,7 @@ use crate::semantic_index::symbol::ScopeId; use crate::semantic_index::{imported_modules, semantic_index}; use crate::suppression::check_suppressions; use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; -use crate::types::call::{Bindings, CallArgumentTypes}; +use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; use crate::types::generics::{GenericContext, Specialization}; @@ -3518,6 +3518,7 @@ impl<'db> Type<'db> { db, name, &mut argument_types, + None, MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) } @@ -3526,11 +3527,18 @@ impl<'db> Type<'db> { /// particular, this allows to specify `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK` to avoid /// looking up dunder methods on `object`, which is needed for functions like `__init__`, /// `__new__`, or `__setattr__`. + /// + /// You can also provide a generic context that will be added to the method's signature before + /// calling it. This is used for the `__new__` and `__init__` methods of an unspecialized + /// generic class, to allow us to infer the _class's_ specialization from the arguments to its + /// constructor. This parameter is _not_ used for functions and methods that are themselves + /// generic; they will already have a generic context attached to them. fn try_call_dunder_with_policy( self, db: &'db dyn Db, name: &str, argument_types: &mut CallArgumentTypes<'_, 'db>, + generic_context: Option>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { match self @@ -3538,7 +3546,11 @@ impl<'db> Type<'db> { .symbol { Symbol::Type(dunder_callable, boundness) => { - let signatures = dunder_callable.signatures(db); + let mut signatures = dunder_callable.signatures(db); + if let Some(generic_context) = generic_context { + eprintln!("==> add generic context to signature {:?}", signatures); + signatures.add_generic_context(generic_context); + } let bindings = Bindings::match_parameters(signatures, argument_types) .check_types(db, argument_types)?; if boundness == Boundness::PossiblyUnbound { @@ -3713,7 +3725,18 @@ impl<'db> Type<'db> { db: &'db dyn Db, mut argument_types: CallArgumentTypes<'_, 'db>, ) -> Result, ConstructorCallError<'db>> { - debug_assert!(matches!(self, Type::ClassLiteral(_) | Type::SubclassOf(_))); + debug_assert!(matches!( + self, + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) + )); + + // If we are trying to construct a non-specialized generic class, we should use the + // constructor parameters to try to infer the class specialization. + let generic_origin = match self { + Type::ClassLiteral(ClassLiteralType::Generic(generic)) => Some(generic), + _ => None, + }; + let generic_context = generic_origin.map(|origin| origin.generic_context(db)); // As of now we do not model custom `__call__` on meta-classes, so the code below // only deals with interplay between `__new__` and `__init__` methods. @@ -3744,6 +3767,7 @@ impl<'db> Type<'db> { db, "__new__", argument_types, + generic_context, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ); @@ -3756,7 +3780,7 @@ impl<'db> Type<'db> { // TODO: we should use the actual return type of `__new__` to determine the instance type let instance_ty = self .to_instance(db) - .expect("Class literal type and subclass-of types should always be convertible to instance type"); + .expect("Class literal, generic alias, and subclass-of types should always be convertible to instance type"); let init_call_outcome = if new_call_outcome.is_none() || !instance_ty @@ -3768,14 +3792,51 @@ impl<'db> Type<'db> { .symbol .is_unbound() { - Some(instance_ty.try_call_dunder(db, "__init__", argument_types)) + Some(instance_ty.try_call_dunder_with_policy( + db, + "__init__", + &mut argument_types, + generic_context, + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + )) } else { None }; match (new_call_outcome, init_call_outcome) { // All calls are successful or not called at all - (None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty), + (new_call_outcome @ (None | Some(Ok(_))), init_call_outcome @ (None | Some(Ok(_)))) => { + let new_specialization = new_call_outcome + .and_then(Result::ok) + .as_ref() + .and_then(Bindings::single_element) + .and_then(CallableBinding::matching_overload) + .and_then(|(_, binding)| binding.specialization()); + if let Some(new_specialization) = &new_specialization { + eprintln!("==> new {}", new_specialization.display(db)); + } + let init_specialization = init_call_outcome + .and_then(Result::ok) + .as_ref() + .and_then(Bindings::single_element) + .and_then(CallableBinding::matching_overload) + .and_then(|(_, binding)| binding.specialization()); + if let Some(init_specialization) = &init_specialization { + eprintln!("==> init {}", init_specialization.display(db)); + } + let specialized = generic_origin + .zip(new_specialization.or(init_specialization)) + .map(|(origin, specialization)| { + Type::instance(ClassType::Generic(GenericAlias::new( + db, + origin, + specialization, + ))) + }) + .unwrap_or(instance_ty); + Ok(specialized) + } + (None | Some(Ok(_)), Some(Err(error))) => { // no custom `__new__` or it was called and succeeded, but `__init__` failed. Err(ConstructorCallError::Init(instance_ty, error)) diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index a2761f91cb43f..27c10e5432a0d 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -5,7 +5,7 @@ use crate::Db; mod arguments; mod bind; pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments}; -pub(super) use bind::Bindings; +pub(super) use bind::{Bindings, CallableBinding}; /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was /// unsuccessful. diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 82881f7bfe4cf..7d1362c037fb4 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -16,7 +16,7 @@ use crate::types::diagnostic::{ NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; -use crate::types::generics::SpecializationBuilder; +use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ todo_type, BoundMethodType, FunctionDecorators, KnownClass, KnownFunction, KnownInstanceType, @@ -148,6 +148,13 @@ impl<'db> Bindings<'db> { self.elements.len() == 1 } + pub(crate) fn single_element(&self) -> Option<&CallableBinding<'db>> { + match self.elements.as_slice() { + [element] => Some(element), + _ => None, + } + } + pub(crate) fn callable_type(&self) -> Type<'db> { self.signatures.callable_type } @@ -823,6 +830,9 @@ pub(crate) struct Binding<'db> { /// Return type of the call. return_ty: Type<'db>, + /// The specialization that was inferred from the argument types, if the callable is generic. + specialization: Option>, + /// The formal parameter that each argument is matched with, in argument source order, or /// `None` if the argument was not matched to any parameter. argument_parameters: Box<[Option]>, @@ -958,6 +968,7 @@ impl<'db> Binding<'db> { Self { return_ty: signature.return_ty.unwrap_or(Type::unknown()), + specialization: None, argument_parameters: argument_parameters.into_boxed_slice(), parameter_tys: vec![None; parameters.len()].into_boxed_slice(), errors, @@ -970,10 +981,11 @@ impl<'db> Binding<'db> { signature: &Signature<'db>, argument_types: &CallArgumentTypes<'_, 'db>, ) { + eprintln!("==> check types {:?}", signature); // If this overload is generic, first see if we can infer a specialization of the function // from the arguments that were passed in. let parameters = signature.parameters(); - let specialization = signature.generic_context.map(|generic_context| { + self.specialization = signature.generic_context.map(|generic_context| { let mut builder = SpecializationBuilder::new(db, generic_context); for (argument_index, (_, argument_type)) in argument_types.iter().enumerate() { let Some(parameter_index) = self.argument_parameters[argument_index] else { @@ -1013,7 +1025,7 @@ impl<'db> Binding<'db> { }; let parameter = ¶meters[parameter_index]; if let Some(mut expected_ty) = parameter.annotated_type() { - if let Some(specialization) = specialization { + if let Some(specialization) = self.specialization { expected_ty = expected_ty.apply_specialization(db, specialization); } if !argument_type.is_assignable_to(db, expected_ty) { @@ -1038,7 +1050,7 @@ impl<'db> Binding<'db> { } } - if let Some(specialization) = specialization { + if let Some(specialization) = self.specialization { self.return_ty = self.return_ty.apply_specialization(db, specialization); } } @@ -1051,6 +1063,10 @@ impl<'db> Binding<'db> { self.return_ty } + pub(crate) fn specialization(&self) -> Option> { + self.specialization + } + pub(crate) fn parameter_types(&self) -> &[Option>] { &self.parameter_tys } diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index c714689881091..f285a1476b71b 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -189,6 +189,11 @@ impl<'db> SpecializationBuilder<'db> { } pub(crate) fn infer(&mut self, formal: Type<'db>, actual: Type<'db>) { + eprintln!( + "==> infer {} {}", + formal.display(self.db), + actual.display(self.db) + ); // If the actual type is already assignable to the formal type, then return without adding // any new type mappings. (Note that if the formal type contains any typevars, this check // will fail, since no non-typevar types are assignable to a typevar.) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 3d7bf38616f15..81efd5f8fd869 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2516,6 +2516,7 @@ impl<'db> TypeInferenceBuilder<'db> { )), value_ty, ]), + None, MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); @@ -4038,14 +4039,10 @@ impl<'db> TypeInferenceBuilder<'db> { // For class literals we model the entire class instantiation logic, so it is handled // in a separate function. let class = match callable_type { - Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Dynamic(_) => None, - ClassBase::Class(class) => { - let (class_literal, _) = class.class_literal(self.db()); - Some(class_literal) - } - }, - Type::ClassLiteral(class) => Some(class), + Type::ClassLiteral(_) | Type::GenericAlias(_) => Some(callable_type), + Type::SubclassOf(subclass) if matches!(subclass.subclass_of(), ClassBase::Class(_)) => { + Some(callable_type) + } _ => None, }; @@ -4054,16 +4051,19 @@ impl<'db> TypeInferenceBuilder<'db> { // below. TODO: it should be possible to move these special cases into the // `try_call_constructor` path instead, or even remove some entirely once we support // overloads fully. - class.known(self.db()).is_none_or(|class| { - !matches!( - class, - KnownClass::Bool - | KnownClass::Str - | KnownClass::Type - | KnownClass::Object - | KnownClass::Property - ) - }) + match class { + Type::ClassLiteral(class) => class.known(self.db()).is_none_or(|class| { + !matches!( + class, + KnownClass::Bool + | KnownClass::Str + | KnownClass::Type + | KnownClass::Object + | KnownClass::Property + ) + }), + _ => true, + } }) { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; let call_argument_types = diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 7f89c24a896bb..67c75036b4497 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -80,6 +80,12 @@ impl<'db> Signatures<'db> { } } + pub(crate) fn add_generic_context(&mut self, generic_context: GenericContext<'db>) { + for signature in &mut self.elements { + signature.add_generic_context(generic_context); + } + } + pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) { for signature in &mut self.elements { signature.dunder_call_is_possibly_unbound = true; @@ -195,6 +201,12 @@ impl<'db> CallableSignature<'db> { self.callable_type = after; } } + + fn add_generic_context(&mut self, generic_context: GenericContext<'db>) { + for overload in &mut self.overloads { + overload.add_generic_context(generic_context); + } + } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { @@ -283,6 +295,10 @@ impl<'db> Signature<'db> { } } + fn add_generic_context(&mut self, generic_context: GenericContext<'db>) { + self.generic_context = Some(generic_context); + } + pub(crate) fn apply_specialization( &mut self, db: &'db dyn Db, From 5a4bd28197c3e7e08c935e30dc11067128df57b7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Apr 2025 16:32:07 -0400 Subject: [PATCH 083/100] TODO: constructor is getting specialized with unknown --- crates/red_knot_python_semantic/src/types.rs | 32 +++++++++-- .../src/types/call/bind.rs | 1 - .../src/types/class.rs | 56 +++++++++++++++++-- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b2a151a48e343..310c9ac58cd23 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -182,6 +182,11 @@ bitflags! { /// /// This is similar to no object fallback above const META_CLASS_NO_TYPE_FALLBACK = 1 << 2; + + /// When looking up an attribute of an unspecialized generic class, do _not_ apply the + /// default specialization to the attribute's type. That means the result might contain + /// unspecialized typevars, which the caller must be able to handle correctly. + const DO_NOT_SPECIALIZE_SELF_MEMBERS = 1 << 3; } } @@ -204,6 +209,12 @@ impl MemberLookupPolicy { pub(crate) const fn meta_class_no_type_fallback(self) -> bool { self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) } + + /// Do _not_ apply the default specialization to the type of attributes of an unspecialized + /// generic class. + pub(crate) const fn do_not_specialize_self_members(self) -> bool { + self.contains(Self::DO_NOT_SPECIALIZE_SELF_MEMBERS) + } } impl Default for MemberLookupPolicy { @@ -2020,7 +2031,7 @@ impl<'db> Type<'db> { "__get__" | "__set__" | "__delete__", ) => Some(Symbol::Unbound.into()), - _ => Some(class.class_member(db, None, name, policy)), + _ => Some(class.class_member(db, name, policy)), } } @@ -3548,7 +3559,6 @@ impl<'db> Type<'db> { Symbol::Type(dunder_callable, boundness) => { let mut signatures = dunder_callable.signatures(db); if let Some(generic_context) = generic_context { - eprintln!("==> add generic context to signature {:?}", signatures); signatures.add_generic_context(generic_context); } let bindings = Bindings::match_parameters(signatures, argument_types) @@ -3769,7 +3779,8 @@ impl<'db> Type<'db> { argument_types, generic_context, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK + | MemberLookupPolicy::DO_NOT_SPECIALIZE_SELF_MEMBERS, ); match result { Err(CallDunderError::MethodNotAvailable) => None, @@ -3797,7 +3808,8 @@ impl<'db> Type<'db> { "__init__", &mut argument_types, generic_context, - MemberLookupPolicy::NO_INSTANCE_FALLBACK, + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::DO_NOT_SPECIALIZE_SELF_MEMBERS, )) } else { None @@ -5477,6 +5489,18 @@ impl<'db> FunctionType<'db> { self.known(db) == Some(known_function) } + fn with_generic_context(self, db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { + Self::new( + db, + self.name(db).clone(), + self.known(db), + self.body_scope(db), + self.decorators(db), + Some(generic_context), + self.specialization(db), + ) + } + fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { let specialization = match self.specialization(db) { Some(existing) => existing.apply_specialization(db, specialization), diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 7d1362c037fb4..26589dfc27ad7 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -981,7 +981,6 @@ impl<'db> Binding<'db> { signature: &Signature<'db>, argument_types: &CallArgumentTypes<'_, 'db>, ) { - eprintln!("==> check types {:?}", signature); // If this overload is generic, first see if we can infer a specialization of the function // from the arguments that were passed in. let parameters = signature.parameters(); diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 7585ee66e376c..26b5d48ccf07a 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -277,8 +277,9 @@ impl<'db> ClassType<'db> { policy: MemberLookupPolicy, ) -> SymbolAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); + eprintln!("==> class type class member {}", name); class_literal - .class_member(db, specialization, name, policy) + .class_member_inner(db, None, specialization, name, policy) .map_type(|ty| self.specialize_type(db, ty)) } @@ -692,6 +693,16 @@ impl<'db> ClassLiteralType<'db> { pub(super) fn class_member( self, db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> SymbolAndQualifiers<'db> { + self.class_member_inner(db, Some(self), None, name, policy) + } + + fn class_member_inner( + self, + db: &'db dyn Db, + self_type: Option>, specialization: Option>, name: &str, policy: MemberLookupPolicy, @@ -712,7 +723,7 @@ impl<'db> ClassLiteralType<'db> { let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound(TypeQualifiers::empty())); - for superclass in self.iter_mro(db, specialization) { + for (idx, superclass) in self.iter_mro(db, specialization).enumerate() { match superclass { ClassBase::Dynamic(DynamicType::TodoProtocol) => { // TODO: We currently skip `Protocol` when looking up class members, in order to @@ -739,7 +750,24 @@ impl<'db> ClassLiteralType<'db> { } lookup_result = lookup_result.or_else(|lookup_error| { - lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) + let member = match self_type { + Some(self_type) + if idx == 0 && policy.do_not_specialize_self_members() => + { + self_type.own_class_member(db, name) + } + _ => class.own_class_member(db, name), + }; + let x = member; + eprintln!( + "==> own class member {} {}", + name, + x.symbol + .ignore_possibly_unbound() + .unwrap_or(Type::unknown()) + .display(db) + ); + lookup_error.or_fall_back_to(db, x) }); } } @@ -786,7 +814,26 @@ impl<'db> ClassLiteralType<'db> { /// traverse through the MRO until it finds the member. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { let body_scope = self.body_scope(db); - class_symbol(db, body_scope, name) + class_symbol(db, body_scope, name).map_type(|ty| { + // The `__new__` and `__init__` members of a non-specialized generic class are handled + // specially: they inherit the generic context of their class. That lets us treat them + // as generic functions when constructing the class, and infer the specialization of + // the class from the arguments that are passed in. + match (self, ty, name) { + ( + ClassLiteralType::Generic(origin), + Type::FunctionLiteral(function), + "__new__" | "__init__", + ) => { + let f = Type::FunctionLiteral( + function.with_generic_context(db, origin.generic_context(db)), + ); + eprintln!("==> inherit generic {} {}", ty.display(db), f.display(db)); + f + } + _ => ty, + } + }) } /// Returns the `name` attribute of an instance of this class. @@ -822,6 +869,7 @@ impl<'db> ClassLiteralType<'db> { qualifiers, } = class.own_instance_member(db, name) { + eprintln!("==> own instance member {} {}", name, ty.display(db)); // TODO: We could raise a diagnostic here if there are conflicting type qualifiers union_qualifiers |= qualifiers; From 756ce9af832131234c8e17f2e9f7a7dca2300a11 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 14:54:00 -0400 Subject: [PATCH 084/100] Add uninitialized instance variant --- crates/red_knot_python_semantic/src/types.rs | 122 +++++++++--------- .../src/types/builder.rs | 8 +- .../src/types/class.rs | 23 +++- .../src/types/display.rs | 24 +++- .../src/types/infer.rs | 29 +++-- .../src/types/type_ordering.rs | 17 ++- 6 files changed, 137 insertions(+), 86 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 310c9ac58cd23..b0cdd19e1ee4e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -412,17 +412,20 @@ impl<'db> Type<'db> { fn is_none(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) + .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::NoneType)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) + self.into_instance().is_some_and(|instance| { + instance + .class(db) + .is_known(db, KnownClass::NotImplementedType) + }) } pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class.is_object(db)) + .is_some_and(|instance| instance.class(db).is_object(db)) } pub const fn is_todo(&self) -> bool { @@ -684,7 +687,7 @@ impl<'db> Type<'db> { } pub const fn instance(class: ClassType<'db>) -> Self { - Self::Instance(InstanceType { class }) + Self::Instance(InstanceType::Class(class)) } pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { @@ -815,7 +818,7 @@ impl<'db> Type<'db> { (_, Type::Never) => false, // Everything is a subtype of `object`. - (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, + (_, Type::Instance(InstanceType::Class(class))) if class.is_object(db) => true, // A fully static typevar is always a subtype of itself, and is never a subtype of any // other typevar, since there is no guarantee that they will be specialized to the same @@ -1083,7 +1086,7 @@ impl<'db> Type<'db> { // All types are assignable to `object`. // TODO this special case might be removable once the below cases are comprehensive - (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, + (_, Type::Instance(InstanceType::Class(class))) if class.is_object(db) => true, // A typevar is always assignable to itself, and is never assignable to any other // typevar, since there is no guarantee that they will be specialized to the same @@ -1236,11 +1239,11 @@ impl<'db> Type<'db> { // TODO: This is a workaround to avoid false positives (e.g. when checking function calls // with `SupportsIndex` parameters), which should be removed when we understand protocols. - (lhs, Type::Instance(InstanceType { class })) + (lhs, Type::Instance(InstanceType::Class(class))) if class.is_known(db, KnownClass::SupportsIndex) => { match lhs { - Type::Instance(InstanceType { class }) + Type::Instance(InstanceType::Class(class)) if matches!( class.known(db), Some(KnownClass::Int | KnownClass::SupportsIndex) @@ -1254,7 +1257,7 @@ impl<'db> Type<'db> { } // TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters. - (lhs, Type::Instance(InstanceType { class })) + (lhs, Type::Instance(InstanceType::Class(class))) if class.is_known(db, KnownClass::Sized) => { matches!( @@ -1568,9 +1571,9 @@ impl<'db> Type<'db> { .is_disjoint_from(db, other), }, - (Type::KnownInstance(known_instance), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => { - !known_instance.is_instance_of(db, class) + (Type::KnownInstance(known_instance), Type::Instance(instance)) + | (Type::Instance(instance), Type::KnownInstance(known_instance)) => { + !known_instance.is_instance_of(db, instance.class(db)) } (known_instance_ty @ Type::KnownInstance(_), Type::Tuple(_)) @@ -1578,20 +1581,20 @@ impl<'db> Type<'db> { known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) } - (Type::BooleanLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => { + (Type::BooleanLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::BooleanLiteral(..)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - !KnownClass::Bool.is_subclass_of(db, class) + !KnownClass::Bool.is_subclass_of(db, instance.class(db)) } (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, - (Type::IntLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::IntLiteral(..)) => { + (Type::IntLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::IntLiteral(..)) => { // A `Type::IntLiteral()` must be an instance of exactly `int` // (it cannot be an instance of an `int` subclass) - !KnownClass::Int.is_subclass_of(db, class) + !KnownClass::Int.is_subclass_of(db, instance.class(db)) } (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, @@ -1599,34 +1602,28 @@ impl<'db> Type<'db> { (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, - ( - Type::StringLiteral(..) | Type::LiteralString, - Type::Instance(InstanceType { class }), - ) - | ( - Type::Instance(InstanceType { class }), - Type::StringLiteral(..) | Type::LiteralString, - ) => { + (Type::StringLiteral(..) | Type::LiteralString, Type::Instance(instance)) + | (Type::Instance(instance), Type::StringLiteral(..) | Type::LiteralString) => { // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // (it cannot be an instance of a `str` subclass) - !KnownClass::Str.is_subclass_of(db, class) + !KnownClass::Str.is_subclass_of(db, instance.class(db)) } (Type::LiteralString, Type::LiteralString) => false, (Type::LiteralString, _) | (_, Type::LiteralString) => true, - (Type::BytesLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::BytesLiteral(..)) => { + (Type::BytesLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::BytesLiteral(..)) => { // A `Type::BytesLiteral()` must be an instance of exactly `bytes` // (it cannot be an instance of a `bytes` subclass) - !KnownClass::Bytes.is_subclass_of(db, class) + !KnownClass::Bytes.is_subclass_of(db, instance.class(db)) } - (Type::SliceLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::SliceLiteral(..)) => { + (Type::SliceLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::SliceLiteral(..)) => { // A `Type::SliceLiteral` must be an instance of exactly `slice` // (it cannot be an instance of a `slice` subclass) - !KnownClass::Slice.is_subclass_of(db, class) + !KnownClass::Slice.is_subclass_of(db, instance.class(db)) } // A class-literal type `X` is always disjoint from an instance type `Y`, @@ -1641,11 +1638,11 @@ impl<'db> Type<'db> { .metaclass_instance_type(db) .is_subtype_of(db, instance), - (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => { + (Type::FunctionLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) - !KnownClass::FunctionType.is_subclass_of(db, class) + !KnownClass::FunctionType.is_subclass_of(db, instance.class(db)) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType @@ -1697,10 +1694,9 @@ impl<'db> Type<'db> { other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db)) } - ( - Type::Instance(InstanceType { class: left_class }), - Type::Instance(InstanceType { class: right_class }), - ) => { + (Type::Instance(left_instance), Type::Instance(right_instance)) => { + let left_class = left_instance.class(db); + let right_class = right_instance.class(db); (left_class.is_final(db) && !left_class.is_subclass_of(db, right_class)) || (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class)) } @@ -1861,9 +1857,10 @@ impl<'db> Type<'db> { // (this variant represents `f.__get__`, where `f` is any function) false } - Type::Instance(InstanceType { class }) => { - class.known(db).is_some_and(KnownClass::is_singleton) - } + Type::Instance(instance) => instance + .class(db) + .known(db) + .is_some_and(KnownClass::is_singleton), Type::PropertyInstance(_) => false, Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -1935,9 +1932,10 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_single_valued(db)), - Type::Instance(InstanceType { class }) => { - class.known(db).is_some_and(KnownClass::is_single_valued) - } + Type::Instance(instance) => instance + .class(db) + .known(db) + .is_some_and(KnownClass::is_single_valued), Type::Dynamic(_) | Type::Never @@ -2065,7 +2063,7 @@ impl<'db> Type<'db> { // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type). // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the // MRO of the class `object`. - Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => { + Type::Instance(instance) if instance.class(db).is_known(db, KnownClass::Type) => { KnownClass::Object .to_class_literal(db) .find_name_in_mro_with_policy(db, name, policy) @@ -2153,7 +2151,10 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), - Type::Instance(InstanceType { class }) => class.instance_member(db, name), + Type::Instance(InstanceType::Class(class)) => class.instance_member(db, name), + Type::Instance(InstanceType::UninitializedGenericClass(generic)) => { + generic.instance_member(db, None, name) + } Type::FunctionLiteral(_) => KnownClass::FunctionType .to_instance(db) @@ -2568,9 +2569,9 @@ impl<'db> Type<'db> { .member(db, &name), Type::Callable(_) => KnownClass::Object.to_instance(db).member(db, &name), - Type::Instance(InstanceType { class }) + Type::Instance(instance) if matches!(name.as_str(), "major" | "minor") - && class.is_known(db, KnownClass::VersionInfo) => + && instance.class(db).is_known(db, KnownClass::VersionInfo) => { let python_version = Program::get(db).python_version(db); let segment = if name == "major" { @@ -2581,7 +2582,7 @@ impl<'db> Type<'db> { Symbol::bound(Type::IntLiteral(segment.into())).into() } - Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Super) => { + Type::Instance(instance) if instance.class(db).is_known(db, KnownClass::Super) => { SymbolAndQualifiers::todo("super() support") } @@ -2645,8 +2646,8 @@ impl<'db> Type<'db> { // It will need a special handling, so it remember the origin type to properly // resolve the attribute. if self.into_instance().is_some_and(|instance| { - instance.class.is_known(db, KnownClass::ModuleType) - || instance.class.is_known(db, KnownClass::GenericAlias) + instance.class(db).is_known(db, KnownClass::ModuleType) + || instance.class(db).is_known(db, KnownClass::GenericAlias) }) { return Symbol::Unbound.into(); } @@ -2890,7 +2891,7 @@ impl<'db> Type<'db> { } }, - Type::Instance(InstanceType { class }) => match class.known(db) { + Type::Instance(instance) => match instance.class(db).known(db) { Some(known_class) => known_class.bool(), None => try_dunder_bool()?, }, @@ -4079,7 +4080,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) => Ok(*self), - Type::Instance(InstanceType { class }) => match class.known(db) { + Type::Instance(instance) => match instance.class(db).known(db) { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), @@ -4152,7 +4153,10 @@ impl<'db> Type<'db> { pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, - Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class), + Type::Instance(InstanceType::Class(class)) => SubclassOfType::from(db, *class), + Type::Instance(InstanceType::UninitializedGenericClass(generic)) => { + Type::from(*generic) + } Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db), Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), @@ -4375,7 +4379,9 @@ impl<'db> Type<'db> { Some(TypeDefinition::Class(class_literal.definition(db))) } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), - Self::Instance(instance) => Some(TypeDefinition::Class(instance.class.definition(db))), + Self::Instance(instance) => { + Some(TypeDefinition::Class(instance.class(db).definition(db))) + } Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db))) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 915a88bfe707c..f58de93f8c7dc 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -297,7 +297,7 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive .into_instance() - .and_then(|instance| instance.class.known(db)); + .and_then(|instance| instance.class(db).known(db)); if known_instance == Some(KnownClass::Object) { // `object & T` -> `T`; it is always redundant to add `object` to an intersection @@ -317,7 +317,7 @@ impl<'db> InnerIntersectionBuilder<'db> { new_positive = Type::BooleanLiteral(false); } Type::Instance(instance) - if instance.class.is_known(db, KnownClass::Bool) => + if instance.class(db).is_known(db, KnownClass::Bool) => { match new_positive { // `bool & AlwaysTruthy` -> `Literal[True]` @@ -411,7 +411,7 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive .iter() .filter_map(|ty| ty.into_instance()) - .filter_map(|instance| instance.class.known(db)) + .filter_map(|instance| instance.class(db).known(db)) .any(KnownClass::is_bool) }; @@ -427,7 +427,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::Instance(instance) if instance.class.is_object(db) => { + Type::Instance(instance) if instance.class(db).is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 26b5d48ccf07a..2421bfe496480 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1166,14 +1166,31 @@ impl InheritanceCycle { /// A type representing the set of runtime objects which are instances of a certain class. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] -pub struct InstanceType<'db> { - pub class: ClassType<'db>, +pub enum InstanceType<'db> { + /// Once constructed, an object is always an instance of a class type — if needed, we will have + /// applied the default specialization to a generic class to produce the generic alias that the + /// object is an instance of. + Class(ClassType<'db>), + + /// While constructing a non-specialized generic class, we want to look up the `__init__` + /// method as an instance method _without_ applying default specialization. In only this one + /// case, we consider the (not yet initialized) object to be an instance of the generic class, + /// since we don't know what specialization to apply until we have inferred one from the + /// `__init__` arguments. + UninitializedGenericClass(ClassLiteralType<'db>), } impl<'db> InstanceType<'db> { + pub fn class(self, db: &'db dyn Db) -> ClassType<'db> { + match self { + InstanceType::Class(class) => class, + InstanceType::UninitializedGenericClass(generic) => generic.default_specialization(db), + } + } + pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { // N.B. The subclass relation is fully static - self.class.is_subclass_of(db, other.class) + self.class(db).is_subclass_of(db, other.class(db)) } } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 87825bc5a684c..bceeb7804a9f0 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -6,7 +6,7 @@ use ruff_db::display::FormatterJoinExtension; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; -use crate::types::class::{ClassType, GenericAlias, GenericClass}; +use crate::types::class::{ClassLiteralType, ClassType, GenericAlias, GenericClass}; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; use crate::types::signatures::{Parameter, Parameters, Signature}; @@ -72,11 +72,21 @@ impl Display for DisplayRepresentation<'_> { match self.ty { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), - Type::Instance(InstanceType { class }) => match (class, class.known(self.db)) { + Type::Instance(instance) => match (instance, instance.class(self.db).known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - (ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name), - (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), + (InstanceType::Class(ClassType::NonGeneric(class)), _) + | ( + InstanceType::UninitializedGenericClass(ClassLiteralType::NonGeneric(class)), + _, + ) => f.write_str(&class.class(self.db).name), + (InstanceType::Class(ClassType::Generic(alias)), _) => { + write!(f, "{}", alias.display(self.db)) + } + ( + InstanceType::UninitializedGenericClass(ClassLiteralType::Generic(generic)), + _, + ) => f.write_str(&generic.class(self.db).name), }, Type::PropertyInstance(_) => f.write_str("property"), Type::ModuleLiteral(module) => { @@ -106,9 +116,9 @@ impl Display for DisplayRepresentation<'_> { let function = bound_method.function(self.db); let self_instance = bound_method.self_instance(self.db); let self_instance_specialization = match self_instance { - Type::Instance(InstanceType { - class: ClassType::Generic(alias), - }) => Some(alias.specialization(self.db)), + Type::Instance(InstanceType::Class(ClassType::Generic(alias))) => { + Some(alias.specialization(self.db)) + } _ => None, }; let specialization = match function.specialization(self.db) { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 81efd5f8fd869..c9f582c02df55 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -981,7 +981,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::Instance(instance) if matches!( - instance.class.known(self.db()), + instance.class(self.db()).known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) ) => {} _ => return false, @@ -2882,7 +2882,10 @@ impl<'db> TypeInferenceBuilder<'db> { // Handle various singletons. if let Type::Instance(instance) = declared_ty.inner_type() { - if instance.class.is_known(self.db(), KnownClass::SpecialForm) { + if instance + .class(self.db()) + .is_known(self.db(), KnownClass::SpecialForm) + { if let Some(name_expr) = target.as_name_expr() { if let Some(known_instance) = KnownInstanceType::try_from_file_and_name( self.db(), @@ -5480,7 +5483,9 @@ impl<'db> TypeInferenceBuilder<'db> { range, ), (Type::Tuple(_), Type::Instance(instance)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + if instance + .class(self.db()) + .is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( left, @@ -5490,7 +5495,9 @@ impl<'db> TypeInferenceBuilder<'db> { ) } (Type::Instance(instance), Type::Tuple(_)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + if instance + .class(self.db()) + .is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( Type::version_info_tuple(self.db()), @@ -5873,12 +5880,16 @@ impl<'db> TypeInferenceBuilder<'db> { ( Type::Instance(instance), Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_), - ) if instance.class.is_known(self.db(), KnownClass::VersionInfo) => self - .infer_subscript_expression_types( + ) if instance + .class(self.db()) + .is_known(self.db(), KnownClass::VersionInfo) => + { + self.infer_subscript_expression_types( value_node, Type::version_info_tuple(self.db()), slice_ty, - ), + ) + } // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => { @@ -6154,7 +6165,9 @@ impl<'db> TypeInferenceBuilder<'db> { }, Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), Some(Type::Instance(instance)) - if instance.class.is_known(self.db(), KnownClass::NoneType) => + if instance + .class(self.db()) + .is_known(self.db(), KnownClass::NoneType) => { SliceArg::Arg(None) } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index c3f8177266313..9ae2df94db77f 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -108,16 +108,21 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( } } } - (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, + + (Type::Instance(InstanceType::Class(left)), Type::Instance(InstanceType::Class(right))) => { + left.cmp(right) + } + (Type::Instance(InstanceType::Class(_)), _) => Ordering::Less, + (_, Type::Instance(InstanceType::Class(_))) => Ordering::Greater, + ( - Type::Instance(InstanceType { class: left }), - Type::Instance(InstanceType { class: right }), + Type::Instance(InstanceType::UninitializedGenericClass(left)), + Type::Instance(InstanceType::UninitializedGenericClass(right)), ) => left.cmp(right), - - (Type::Instance(_), _) => Ordering::Less, - (_, Type::Instance(_)) => Ordering::Greater, + (Type::Instance(InstanceType::UninitializedGenericClass(_)), _) => Ordering::Less, + (_, Type::Instance(InstanceType::UninitializedGenericClass(_))) => Ordering::Greater, (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), (Type::TypeVar(_), _) => Ordering::Less, From 1f67883e83b57cba94bf54e9babdfc544c8fbec2 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 15:02:52 -0400 Subject: [PATCH 085/100] Create uninitialized instance when constructing --- .../resources/mdtest/generics/classes.md | 13 +--- crates/red_knot_python_semantic/src/types.rs | 63 +++++++------------ .../src/types/class.rs | 29 ++------- .../src/types/generics.rs | 5 -- .../src/types/infer.rs | 1 - 5 files changed, 28 insertions(+), 83 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 8144e1ac459a5..34e3c3fd192f0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -156,8 +156,7 @@ parameter: class E[T]: def __init__(self, x: T) -> None: ... -# TODO: revealed: E[int] or E[Literal[1]] -reveal_type(E(1)) # revealed: E[int] +reveal_type(E(1)) # revealed: E[Literal[1]] ``` The types inferred from a type context and from a constructor parameter must be consistent with each @@ -168,16 +167,6 @@ other: wrong_innards: E[int] = E("five") ``` -## fwomp - -```py -class E[T]: - def __init__(self, x: T) -> None: ... - -# TODO: revealed: E[int] or E[Literal[1]] -reveal_type(E(1)) # revealed: E[int] -``` - ## Generic subclass When a generic subclass fills its superclass's type parameter with one of its own, the actual types diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b0cdd19e1ee4e..2107678eef104 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -182,11 +182,6 @@ bitflags! { /// /// This is similar to no object fallback above const META_CLASS_NO_TYPE_FALLBACK = 1 << 2; - - /// When looking up an attribute of an unspecialized generic class, do _not_ apply the - /// default specialization to the attribute's type. That means the result might contain - /// unspecialized typevars, which the caller must be able to handle correctly. - const DO_NOT_SPECIALIZE_SELF_MEMBERS = 1 << 3; } } @@ -209,12 +204,6 @@ impl MemberLookupPolicy { pub(crate) const fn meta_class_no_type_fallback(self) -> bool { self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) } - - /// Do _not_ apply the default specialization to the type of attributes of an unspecialized - /// generic class. - pub(crate) const fn do_not_specialize_self_members(self) -> bool { - self.contains(Self::DO_NOT_SPECIALIZE_SELF_MEMBERS) - } } impl Default for MemberLookupPolicy { @@ -3530,7 +3519,6 @@ impl<'db> Type<'db> { db, name, &mut argument_types, - None, MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) } @@ -3539,18 +3527,11 @@ impl<'db> Type<'db> { /// particular, this allows to specify `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK` to avoid /// looking up dunder methods on `object`, which is needed for functions like `__init__`, /// `__new__`, or `__setattr__`. - /// - /// You can also provide a generic context that will be added to the method's signature before - /// calling it. This is used for the `__new__` and `__init__` methods of an unspecialized - /// generic class, to allow us to infer the _class's_ specialization from the arguments to its - /// constructor. This parameter is _not_ used for functions and methods that are themselves - /// generic; they will already have a generic context attached to them. fn try_call_dunder_with_policy( self, db: &'db dyn Db, name: &str, argument_types: &mut CallArgumentTypes<'_, 'db>, - generic_context: Option>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { match self @@ -3558,10 +3539,7 @@ impl<'db> Type<'db> { .symbol { Symbol::Type(dunder_callable, boundness) => { - let mut signatures = dunder_callable.signatures(db); - if let Some(generic_context) = generic_context { - signatures.add_generic_context(generic_context); - } + let signatures = dunder_callable.signatures(db); let bindings = Bindings::match_parameters(signatures, argument_types) .check_types(db, argument_types)?; if boundness == Boundness::PossiblyUnbound { @@ -3747,7 +3725,6 @@ impl<'db> Type<'db> { Type::ClassLiteral(ClassLiteralType::Generic(generic)) => Some(generic), _ => None, }; - let generic_context = generic_origin.map(|origin| origin.generic_context(db)); // As of now we do not model custom `__call__` on meta-classes, so the code below // only deals with interplay between `__new__` and `__init__` methods. @@ -3778,10 +3755,8 @@ impl<'db> Type<'db> { db, "__new__", argument_types, - generic_context, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK - | MemberLookupPolicy::DO_NOT_SPECIALIZE_SELF_MEMBERS, + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ); match result { Err(CallDunderError::MethodNotAvailable) => None, @@ -3790,12 +3765,22 @@ impl<'db> Type<'db> { }); // TODO: we should use the actual return type of `__new__` to determine the instance type - let instance_ty = self - .to_instance(db) - .expect("Class literal, generic alias, and subclass-of types should always be convertible to instance type"); + let init_ty = match self { + Type::ClassLiteral(generic @ ClassLiteralType::Generic(_)) => { + Type::Instance(InstanceType::UninitializedGenericClass(generic)) + } + Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { + Type::Instance(InstanceType::Class(ClassType::NonGeneric(non_generic))) + } + Type::GenericAlias(generic) => { + Type::Instance(InstanceType::Class(ClassType::Generic(generic))) + } + Type::SubclassOf(subclass_of) => subclass_of.to_instance(), + _ => panic!("type should be constructible"), + }; let init_call_outcome = if new_call_outcome.is_none() - || !instance_ty + || !init_ty .member_lookup_with_policy( db, "__init__".into(), @@ -3804,18 +3789,20 @@ impl<'db> Type<'db> { .symbol .is_unbound() { - Some(instance_ty.try_call_dunder_with_policy( + Some(init_ty.try_call_dunder_with_policy( db, "__init__", &mut argument_types, - generic_context, - MemberLookupPolicy::NO_INSTANCE_FALLBACK - | MemberLookupPolicy::DO_NOT_SPECIALIZE_SELF_MEMBERS, + MemberLookupPolicy::NO_INSTANCE_FALLBACK, )) } else { None }; + let instance_ty = self + .to_instance(db) + .expect("Class literal, generic alias, and subclass-of types should always be convertible to instance type"); + match (new_call_outcome, init_call_outcome) { // All calls are successful or not called at all (new_call_outcome @ (None | Some(Ok(_))), init_call_outcome @ (None | Some(Ok(_)))) => { @@ -3825,18 +3812,12 @@ impl<'db> Type<'db> { .and_then(Bindings::single_element) .and_then(CallableBinding::matching_overload) .and_then(|(_, binding)| binding.specialization()); - if let Some(new_specialization) = &new_specialization { - eprintln!("==> new {}", new_specialization.display(db)); - } let init_specialization = init_call_outcome .and_then(Result::ok) .as_ref() .and_then(Bindings::single_element) .and_then(CallableBinding::matching_overload) .and_then(|(_, binding)| binding.specialization()); - if let Some(init_specialization) = &init_specialization { - eprintln!("==> init {}", init_specialization.display(db)); - } let specialized = generic_origin .zip(new_specialization.or(init_specialization)) .map(|(origin, specialization)| { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 2421bfe496480..7d32502a19bca 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -277,7 +277,6 @@ impl<'db> ClassType<'db> { policy: MemberLookupPolicy, ) -> SymbolAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); - eprintln!("==> class type class member {}", name); class_literal .class_member_inner(db, None, specialization, name, policy) .map_type(|ty| self.specialize_type(db, ty)) @@ -751,23 +750,10 @@ impl<'db> ClassLiteralType<'db> { lookup_result = lookup_result.or_else(|lookup_error| { let member = match self_type { - Some(self_type) - if idx == 0 && policy.do_not_specialize_self_members() => - { - self_type.own_class_member(db, name) - } + Some(self_type) if idx == 0 => self_type.own_class_member(db, name), _ => class.own_class_member(db, name), }; - let x = member; - eprintln!( - "==> own class member {} {}", - name, - x.symbol - .ignore_possibly_unbound() - .unwrap_or(Type::unknown()) - .display(db) - ); - lookup_error.or_fall_back_to(db, x) + lookup_error.or_fall_back_to(db, member) }); } } @@ -824,13 +810,9 @@ impl<'db> ClassLiteralType<'db> { ClassLiteralType::Generic(origin), Type::FunctionLiteral(function), "__new__" | "__init__", - ) => { - let f = Type::FunctionLiteral( - function.with_generic_context(db, origin.generic_context(db)), - ); - eprintln!("==> inherit generic {} {}", ty.display(db), f.display(db)); - f - } + ) => Type::FunctionLiteral( + function.with_generic_context(db, origin.generic_context(db)), + ), _ => ty, } }) @@ -869,7 +851,6 @@ impl<'db> ClassLiteralType<'db> { qualifiers, } = class.own_instance_member(db, name) { - eprintln!("==> own instance member {} {}", name, ty.display(db)); // TODO: We could raise a diagnostic here if there are conflicting type qualifiers union_qualifiers |= qualifiers; diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index f285a1476b71b..c714689881091 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -189,11 +189,6 @@ impl<'db> SpecializationBuilder<'db> { } pub(crate) fn infer(&mut self, formal: Type<'db>, actual: Type<'db>) { - eprintln!( - "==> infer {} {}", - formal.display(self.db), - actual.display(self.db) - ); // If the actual type is already assignable to the formal type, then return without adding // any new type mappings. (Note that if the formal type contains any typevars, this check // will fail, since no non-typevar types are assignable to a typevar.) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index c9f582c02df55..d5392eb052577 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2516,7 +2516,6 @@ impl<'db> TypeInferenceBuilder<'db> { )), value_ty, ]), - None, MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); From beb64b8288f795d275ddf92906d0d7ccd007705f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 15:18:21 -0400 Subject: [PATCH 086/100] clippy --- .../src/types/display.rs | 6 +++--- .../src/types/signatures.rs | 16 ---------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index bceeb7804a9f0..605713617941f 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -75,9 +75,9 @@ impl Display for DisplayRepresentation<'_> { Type::Instance(instance) => match (instance, instance.class(self.db).known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - (InstanceType::Class(ClassType::NonGeneric(class)), _) - | ( - InstanceType::UninitializedGenericClass(ClassLiteralType::NonGeneric(class)), + ( + InstanceType::Class(ClassType::NonGeneric(class)) + | InstanceType::UninitializedGenericClass(ClassLiteralType::NonGeneric(class)), _, ) => f.write_str(&class.class(self.db).name), (InstanceType::Class(ClassType::Generic(alias)), _) => { diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 67c75036b4497..7f89c24a896bb 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -80,12 +80,6 @@ impl<'db> Signatures<'db> { } } - pub(crate) fn add_generic_context(&mut self, generic_context: GenericContext<'db>) { - for signature in &mut self.elements { - signature.add_generic_context(generic_context); - } - } - pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) { for signature in &mut self.elements { signature.dunder_call_is_possibly_unbound = true; @@ -201,12 +195,6 @@ impl<'db> CallableSignature<'db> { self.callable_type = after; } } - - fn add_generic_context(&mut self, generic_context: GenericContext<'db>) { - for overload in &mut self.overloads { - overload.add_generic_context(generic_context); - } - } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { @@ -295,10 +283,6 @@ impl<'db> Signature<'db> { } } - fn add_generic_context(&mut self, generic_context: GenericContext<'db>) { - self.generic_context = Some(generic_context); - } - pub(crate) fn apply_specialization( &mut self, db: &'db dyn Db, From c1e3470f30e4b0eaf8c553e30d440d765e81027c Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 15:22:06 -0400 Subject: [PATCH 087/100] Only extract specializations for generic classes --- crates/red_knot_python_semantic/src/types.rs | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3637cc15b6a96..ff2602f032e5a 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3803,9 +3803,13 @@ impl<'db> Type<'db> { .to_instance(db) .expect("Class literal, generic alias, and subclass-of types should always be convertible to instance type"); - match (new_call_outcome, init_call_outcome) { + match (generic_origin, new_call_outcome, init_call_outcome) { // All calls are successful or not called at all - (new_call_outcome @ (None | Some(Ok(_))), init_call_outcome @ (None | Some(Ok(_)))) => { + ( + Some(generic_origin), + new_call_outcome @ (None | Some(Ok(_))), + init_call_outcome @ (None | Some(Ok(_))), + ) => { let new_specialization = new_call_outcome .and_then(Result::ok) .as_ref() @@ -3818,12 +3822,11 @@ impl<'db> Type<'db> { .and_then(Bindings::single_element) .and_then(CallableBinding::matching_overload) .and_then(|(_, binding)| binding.specialization()); - let specialized = generic_origin - .zip(new_specialization.or(init_specialization)) - .map(|(origin, specialization)| { + let specialized = (new_specialization.or(init_specialization)) + .map(|specialization| { Type::instance(ClassType::Generic(GenericAlias::new( db, - origin, + generic_origin, specialization, ))) }) @@ -3831,15 +3834,17 @@ impl<'db> Type<'db> { Ok(specialized) } - (None | Some(Ok(_)), Some(Err(error))) => { + (None, None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty), + + (_, None | Some(Ok(_)), Some(Err(error))) => { // no custom `__new__` or it was called and succeeded, but `__init__` failed. Err(ConstructorCallError::Init(instance_ty, error)) } - (Some(Err(error)), None | Some(Ok(_))) => { + (_, Some(Err(error)), None | Some(Ok(_))) => { // custom `__new__` was called and failed, but init is ok Err(ConstructorCallError::New(instance_ty, error)) } - (Some(Err(new_error)), Some(Err(init_error))) => { + (_, Some(Err(new_error)), Some(Err(init_error))) => { // custom `__new__` was called and failed, and `__init__` is also not ok Err(ConstructorCallError::NewAndInit( instance_ty, From 4bd8128265c810cb62f14dc7e5deb91a76385731 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 21:05:17 -0400 Subject: [PATCH 088/100] Use identity specialization for new/init --- .../resources/mdtest/generics/classes.md | 5 +- .../resources/mdtest/generics/scoping.md | 5 +- crates/red_knot_python_semantic/src/types.rs | 10 ++-- .../src/types/class.rs | 46 ++++++++++++----- .../src/types/display.rs | 50 ++++++++++++++++++- .../src/types/generics.rs | 9 ++++ 6 files changed, 97 insertions(+), 28 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 4d2d2d8f1d0ba..37f3e2f3b2f7a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -199,10 +199,7 @@ class C[T]: def cannot_shadow_class_typevar[T](self, t: T): ... c: C[int] = C[int]() -# TODO: no error -# TODO: revealed: str or Literal["string"] -# error: [invalid-argument-type] -reveal_type(c.method("string")) # revealed: U +reveal_type(c.method("string")) # revealed: Literal["string"] ``` ## Cyclic class definition diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 1147fdd65f1fc..900401cf65e46 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -151,10 +151,7 @@ class C[T]: return y c: C[int] = C() -# TODO: no errors -# TODO: revealed: str -# error: [invalid-argument-type] -reveal_type(c.m(1, "string")) # revealed: S +reveal_type(c.m(1, "string")) # revealed: Literal["string"] ``` ## Unbound typevars diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ff2602f032e5a..d43b987787170 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3767,14 +3767,12 @@ impl<'db> Type<'db> { // TODO: we should use the actual return type of `__new__` to determine the instance type let init_ty = match self { Type::ClassLiteral(generic @ ClassLiteralType::Generic(_)) => { - Type::Instance(InstanceType::UninitializedGenericClass(generic)) + Type::instance(generic.identity_specialization(db)) } Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { - Type::Instance(InstanceType::Class(ClassType::NonGeneric(non_generic))) - } - Type::GenericAlias(generic) => { - Type::Instance(InstanceType::Class(ClassType::Generic(generic))) + Type::instance(ClassType::NonGeneric(non_generic)) } + Type::GenericAlias(generic) => Type::instance(ClassType::Generic(generic)), Type::SubclassOf(subclass_of) => subclass_of.to_instance(), _ => panic!("type should be constructible"), }; @@ -5507,7 +5505,7 @@ impl<'db> FunctionType<'db> { self.known(db), self.body_scope(db), self.decorators(db), - None, // If the function was generic before, it isn't anymore + self.generic_context(db), Some(specialization), ) } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e9264cb415cfa..1a93925943247 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -278,7 +278,7 @@ impl<'db> ClassType<'db> { ) -> SymbolAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal - .class_member_inner(db, None, specialization, name, policy) + .class_member_inner(db, specialization, name, policy) .map_type(|ty| self.specialize_type(db, ty)) } @@ -290,9 +290,23 @@ impl<'db> ClassType<'db> { /// traverse through the MRO until it finds the member. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { let (class_literal, _) = self.class_literal(db); - class_literal - .own_class_member(db, name) - .map_type(|ty| self.specialize_type(db, ty)) + class_literal.own_class_member(db, name).map_type(|ty| { + let specialized = self.specialize_type(db, ty); + // The `__new__` and `__init__` members of a non-specialized generic class are handled + // specially: they inherit the generic context of their class. That lets us treat them + // as generic functions when constructing the class, and infer the specialization of + // the class from the arguments that are passed in. + match (self, specialized, name) { + ( + ClassType::Generic(alias), + Type::FunctionLiteral(function), + "__new__" | "__init__", + ) => Type::FunctionLiteral( + function.with_generic_context(db, alias.origin(db).generic_context(db)), + ), + _ => specialized, + } + }) } /// Returns the `name` attribute of an instance of this class. @@ -415,6 +429,19 @@ impl<'db> ClassLiteralType<'db> { } } + /// Returns the identity specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// applies the identity specialization to the class's typevars. + pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + match self { + Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), + Self::Generic(generic) => { + let specialization = generic.generic_context(db).identity_specialization(db); + ClassType::Generic(GenericAlias::new(db, generic, specialization)) + } + } + } + /// Returns the unknown specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. @@ -695,13 +722,12 @@ impl<'db> ClassLiteralType<'db> { name: &str, policy: MemberLookupPolicy, ) -> SymbolAndQualifiers<'db> { - self.class_member_inner(db, Some(self), None, name, policy) + self.class_member_inner(db, None, name, policy) } fn class_member_inner( self, db: &'db dyn Db, - self_type: Option>, specialization: Option>, name: &str, policy: MemberLookupPolicy, @@ -722,7 +748,7 @@ impl<'db> ClassLiteralType<'db> { let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound(TypeQualifiers::empty())); - for (idx, superclass) in self.iter_mro(db, specialization).enumerate() { + for superclass in self.iter_mro(db, specialization) { match superclass { ClassBase::Dynamic(DynamicType::TodoProtocol) => { // TODO: We currently skip `Protocol` when looking up class members, in order to @@ -749,11 +775,7 @@ impl<'db> ClassLiteralType<'db> { } lookup_result = lookup_result.or_else(|lookup_error| { - let member = match self_type { - Some(self_type) if idx == 0 => self_type.own_class_member(db, name), - _ => class.own_class_member(db, name), - }; - lookup_error.or_fall_back_to(db, member) + lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) }); } } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 605713617941f..407d3130a6b1b 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -8,11 +8,11 @@ use ruff_python_literal::escape::AsciiEscape; use crate::types::class::{ClassLiteralType, ClassType, GenericAlias, GenericClass}; use crate::types::class_base::ClassBase; -use crate::types::generics::Specialization; +use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ InstanceType, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, Type, - TypeVarInstance, UnionType, WrapperDescriptorKind, + TypeVarBoundOrConstraints, TypeVarInstance, UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -243,6 +243,52 @@ impl Display for DisplayGenericAlias<'_> { } } +impl<'db> GenericContext<'db> { + pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> { + DisplayGenericContext { + typevars: self.variables(db), + db, + } + } +} + +pub struct DisplayGenericContext<'db> { + typevars: &'db [TypeVarInstance<'db>], + db: &'db dyn Db, +} + +impl Display for DisplayGenericContext<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char('[')?; + for (idx, var) in self.typevars.iter().enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{}", var.name(self.db))?; + match var.bound_or_constraints(self.db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + write!(f, ": {}", bound.display(self.db))?; + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + f.write_str(": (")?; + for (idx, constraint) in constraints.iter(self.db).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{}", constraint.display(self.db))?; + } + f.write_char(')')?; + } + None => {} + } + if let Some(default_type) = var.default_ty(self.db) { + write!(f, " = {}", default_type.display(self.db))?; + } + } + f.write_char(']') + } +} + impl<'db> Specialization<'db> { /// Renders the specialization in full, e.g. `{T = int, U = str}`. pub fn display(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index c714689881091..a79b8ce9e8e0a 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -86,6 +86,15 @@ impl<'db> GenericContext<'db> { self.specialize(db, types) } + pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + let types = self + .variables(db) + .iter() + .map(|typevar| Type::TypeVar(*typevar)) + .collect(); + self.specialize(db, types) + } + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { let types = vec![Type::unknown(); self.variables(db).len()]; self.specialize(db, types.into()) From 373459909bc56121aa71cf24510c4f2e5f02fee8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 21:09:53 -0400 Subject: [PATCH 089/100] Revert new instance variant --- crates/red_knot_python_semantic/src/types.rs | 122 +++++++++--------- .../src/types/builder.rs | 8 +- .../src/types/class.rs | 23 +--- .../src/types/display.rs | 24 +--- .../src/types/infer.rs | 29 ++--- .../src/types/type_ordering.rs | 17 +-- 6 files changed, 86 insertions(+), 137 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d43b987787170..607032d68e95d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -401,20 +401,17 @@ impl<'db> Type<'db> { fn is_none(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::NoneType)) + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance().is_some_and(|instance| { - instance - .class(db) - .is_known(db, KnownClass::NotImplementedType) - }) + self.into_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) } pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class(db).is_object(db)) + .is_some_and(|instance| instance.class.is_object(db)) } pub const fn is_todo(&self) -> bool { @@ -676,7 +673,7 @@ impl<'db> Type<'db> { } pub const fn instance(class: ClassType<'db>) -> Self { - Self::Instance(InstanceType::Class(class)) + Self::Instance(InstanceType { class }) } pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { @@ -807,7 +804,7 @@ impl<'db> Type<'db> { (_, Type::Never) => false, // Everything is a subtype of `object`. - (_, Type::Instance(InstanceType::Class(class))) if class.is_object(db) => true, + (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, // A fully static typevar is always a subtype of itself, and is never a subtype of any // other typevar, since there is no guarantee that they will be specialized to the same @@ -1075,7 +1072,7 @@ impl<'db> Type<'db> { // All types are assignable to `object`. // TODO this special case might be removable once the below cases are comprehensive - (_, Type::Instance(InstanceType::Class(class))) if class.is_object(db) => true, + (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, // A typevar is always assignable to itself, and is never assignable to any other // typevar, since there is no guarantee that they will be specialized to the same @@ -1228,11 +1225,11 @@ impl<'db> Type<'db> { // TODO: This is a workaround to avoid false positives (e.g. when checking function calls // with `SupportsIndex` parameters), which should be removed when we understand protocols. - (lhs, Type::Instance(InstanceType::Class(class))) + (lhs, Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::SupportsIndex) => { match lhs { - Type::Instance(InstanceType::Class(class)) + Type::Instance(InstanceType { class }) if matches!( class.known(db), Some(KnownClass::Int | KnownClass::SupportsIndex) @@ -1246,7 +1243,7 @@ impl<'db> Type<'db> { } // TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters. - (lhs, Type::Instance(InstanceType::Class(class))) + (lhs, Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Sized) => { matches!( @@ -1560,9 +1557,9 @@ impl<'db> Type<'db> { .is_disjoint_from(db, other), }, - (Type::KnownInstance(known_instance), Type::Instance(instance)) - | (Type::Instance(instance), Type::KnownInstance(known_instance)) => { - !known_instance.is_instance_of(db, instance.class(db)) + (Type::KnownInstance(known_instance), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => { + !known_instance.is_instance_of(db, class) } (known_instance_ty @ Type::KnownInstance(_), Type::Tuple(_)) @@ -1570,20 +1567,20 @@ impl<'db> Type<'db> { known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) } - (Type::BooleanLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::BooleanLiteral(..)) => { + (Type::BooleanLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - !KnownClass::Bool.is_subclass_of(db, instance.class(db)) + !KnownClass::Bool.is_subclass_of(db, class) } (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, - (Type::IntLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::IntLiteral(..)) => { + (Type::IntLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::IntLiteral(..)) => { // A `Type::IntLiteral()` must be an instance of exactly `int` // (it cannot be an instance of an `int` subclass) - !KnownClass::Int.is_subclass_of(db, instance.class(db)) + !KnownClass::Int.is_subclass_of(db, class) } (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, @@ -1591,28 +1588,34 @@ impl<'db> Type<'db> { (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, - (Type::StringLiteral(..) | Type::LiteralString, Type::Instance(instance)) - | (Type::Instance(instance), Type::StringLiteral(..) | Type::LiteralString) => { + ( + Type::StringLiteral(..) | Type::LiteralString, + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::StringLiteral(..) | Type::LiteralString, + ) => { // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // (it cannot be an instance of a `str` subclass) - !KnownClass::Str.is_subclass_of(db, instance.class(db)) + !KnownClass::Str.is_subclass_of(db, class) } (Type::LiteralString, Type::LiteralString) => false, (Type::LiteralString, _) | (_, Type::LiteralString) => true, - (Type::BytesLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::BytesLiteral(..)) => { + (Type::BytesLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::BytesLiteral(..)) => { // A `Type::BytesLiteral()` must be an instance of exactly `bytes` // (it cannot be an instance of a `bytes` subclass) - !KnownClass::Bytes.is_subclass_of(db, instance.class(db)) + !KnownClass::Bytes.is_subclass_of(db, class) } - (Type::SliceLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::SliceLiteral(..)) => { + (Type::SliceLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::SliceLiteral(..)) => { // A `Type::SliceLiteral` must be an instance of exactly `slice` // (it cannot be an instance of a `slice` subclass) - !KnownClass::Slice.is_subclass_of(db, instance.class(db)) + !KnownClass::Slice.is_subclass_of(db, class) } // A class-literal type `X` is always disjoint from an instance type `Y`, @@ -1627,11 +1630,11 @@ impl<'db> Type<'db> { .metaclass_instance_type(db) .is_subtype_of(db, instance), - (Type::FunctionLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::FunctionLiteral(..)) => { + (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) - !KnownClass::FunctionType.is_subclass_of(db, instance.class(db)) + !KnownClass::FunctionType.is_subclass_of(db, class) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType @@ -1683,9 +1686,10 @@ impl<'db> Type<'db> { other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db)) } - (Type::Instance(left_instance), Type::Instance(right_instance)) => { - let left_class = left_instance.class(db); - let right_class = right_instance.class(db); + ( + Type::Instance(InstanceType { class: left_class }), + Type::Instance(InstanceType { class: right_class }), + ) => { (left_class.is_final(db) && !left_class.is_subclass_of(db, right_class)) || (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class)) } @@ -1846,10 +1850,9 @@ impl<'db> Type<'db> { // (this variant represents `f.__get__`, where `f` is any function) false } - Type::Instance(instance) => instance - .class(db) - .known(db) - .is_some_and(KnownClass::is_singleton), + Type::Instance(InstanceType { class }) => { + class.known(db).is_some_and(KnownClass::is_singleton) + } Type::PropertyInstance(_) => false, Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -1921,10 +1924,9 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_single_valued(db)), - Type::Instance(instance) => instance - .class(db) - .known(db) - .is_some_and(KnownClass::is_single_valued), + Type::Instance(InstanceType { class }) => { + class.known(db).is_some_and(KnownClass::is_single_valued) + } Type::Dynamic(_) | Type::Never @@ -2052,7 +2054,7 @@ impl<'db> Type<'db> { // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type). // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the // MRO of the class `object`. - Type::Instance(instance) if instance.class(db).is_known(db, KnownClass::Type) => { + Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => { KnownClass::Object .to_class_literal(db) .find_name_in_mro_with_policy(db, name, policy) @@ -2140,10 +2142,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), - Type::Instance(InstanceType::Class(class)) => class.instance_member(db, name), - Type::Instance(InstanceType::UninitializedGenericClass(generic)) => { - generic.instance_member(db, None, name) - } + Type::Instance(InstanceType { class }) => class.instance_member(db, name), Type::FunctionLiteral(_) => KnownClass::FunctionType .to_instance(db) @@ -2558,9 +2557,9 @@ impl<'db> Type<'db> { .member(db, &name), Type::Callable(_) => KnownClass::Object.to_instance(db).member(db, &name), - Type::Instance(instance) + Type::Instance(InstanceType { class }) if matches!(name.as_str(), "major" | "minor") - && instance.class(db).is_known(db, KnownClass::VersionInfo) => + && class.is_known(db, KnownClass::VersionInfo) => { let python_version = Program::get(db).python_version(db); let segment = if name == "major" { @@ -2571,7 +2570,7 @@ impl<'db> Type<'db> { Symbol::bound(Type::IntLiteral(segment.into())).into() } - Type::Instance(instance) if instance.class(db).is_known(db, KnownClass::Super) => { + Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Super) => { SymbolAndQualifiers::todo("super() support") } @@ -2635,8 +2634,8 @@ impl<'db> Type<'db> { // It will need a special handling, so it remember the origin type to properly // resolve the attribute. if self.into_instance().is_some_and(|instance| { - instance.class(db).is_known(db, KnownClass::ModuleType) - || instance.class(db).is_known(db, KnownClass::GenericAlias) + instance.class.is_known(db, KnownClass::ModuleType) + || instance.class.is_known(db, KnownClass::GenericAlias) }) { return Symbol::Unbound.into(); } @@ -2880,7 +2879,7 @@ impl<'db> Type<'db> { } }, - Type::Instance(instance) => match instance.class(db).known(db) { + Type::Instance(InstanceType { class }) => match class.known(db) { Some(known_class) => known_class.bool(), None => try_dunder_bool()?, }, @@ -4064,7 +4063,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) => Ok(*self), - Type::Instance(instance) => match instance.class(db).known(db) { + Type::Instance(InstanceType { class }) => match class.known(db) { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), @@ -4140,10 +4139,7 @@ impl<'db> Type<'db> { pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, - Type::Instance(InstanceType::Class(class)) => SubclassOfType::from(db, *class), - Type::Instance(InstanceType::UninitializedGenericClass(generic)) => { - Type::from(*generic) - } + Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class), Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db), Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), @@ -4366,9 +4362,7 @@ impl<'db> Type<'db> { Some(TypeDefinition::Class(class_literal.definition(db))) } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), - Self::Instance(instance) => { - Some(TypeDefinition::Class(instance.class(db).definition(db))) - } + Self::Instance(instance) => Some(TypeDefinition::Class(instance.class.definition(db))), Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db))) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index f58de93f8c7dc..915a88bfe707c 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -297,7 +297,7 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive .into_instance() - .and_then(|instance| instance.class(db).known(db)); + .and_then(|instance| instance.class.known(db)); if known_instance == Some(KnownClass::Object) { // `object & T` -> `T`; it is always redundant to add `object` to an intersection @@ -317,7 +317,7 @@ impl<'db> InnerIntersectionBuilder<'db> { new_positive = Type::BooleanLiteral(false); } Type::Instance(instance) - if instance.class(db).is_known(db, KnownClass::Bool) => + if instance.class.is_known(db, KnownClass::Bool) => { match new_positive { // `bool & AlwaysTruthy` -> `Literal[True]` @@ -411,7 +411,7 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive .iter() .filter_map(|ty| ty.into_instance()) - .filter_map(|instance| instance.class(db).known(db)) + .filter_map(|instance| instance.class.known(db)) .any(KnownClass::is_bool) }; @@ -427,7 +427,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::Instance(instance) if instance.class(db).is_object(db) => { + Type::Instance(instance) if instance.class.is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 1a93925943247..85e7f19762dc0 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1169,31 +1169,14 @@ impl InheritanceCycle { /// A type representing the set of runtime objects which are instances of a certain class. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] -pub enum InstanceType<'db> { - /// Once constructed, an object is always an instance of a class type — if needed, we will have - /// applied the default specialization to a generic class to produce the generic alias that the - /// object is an instance of. - Class(ClassType<'db>), - - /// While constructing a non-specialized generic class, we want to look up the `__init__` - /// method as an instance method _without_ applying default specialization. In only this one - /// case, we consider the (not yet initialized) object to be an instance of the generic class, - /// since we don't know what specialization to apply until we have inferred one from the - /// `__init__` arguments. - UninitializedGenericClass(ClassLiteralType<'db>), +pub struct InstanceType<'db> { + pub class: ClassType<'db>, } impl<'db> InstanceType<'db> { - pub fn class(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - InstanceType::Class(class) => class, - InstanceType::UninitializedGenericClass(generic) => generic.default_specialization(db), - } - } - pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { // N.B. The subclass relation is fully static - self.class(db).is_subclass_of(db, other.class(db)) + self.class.is_subclass_of(db, other.class) } } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 407d3130a6b1b..552cf28fb4c93 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -6,7 +6,7 @@ use ruff_db::display::FormatterJoinExtension; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; -use crate::types::class::{ClassLiteralType, ClassType, GenericAlias, GenericClass}; +use crate::types::class::{ClassType, GenericAlias, GenericClass}; use crate::types::class_base::ClassBase; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; @@ -72,21 +72,11 @@ impl Display for DisplayRepresentation<'_> { match self.ty { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), - Type::Instance(instance) => match (instance, instance.class(self.db).known(self.db)) { + Type::Instance(InstanceType { class }) => match (class, class.known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - ( - InstanceType::Class(ClassType::NonGeneric(class)) - | InstanceType::UninitializedGenericClass(ClassLiteralType::NonGeneric(class)), - _, - ) => f.write_str(&class.class(self.db).name), - (InstanceType::Class(ClassType::Generic(alias)), _) => { - write!(f, "{}", alias.display(self.db)) - } - ( - InstanceType::UninitializedGenericClass(ClassLiteralType::Generic(generic)), - _, - ) => f.write_str(&generic.class(self.db).name), + (ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name), + (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), }, Type::PropertyInstance(_) => f.write_str("property"), Type::ModuleLiteral(module) => { @@ -116,9 +106,9 @@ impl Display for DisplayRepresentation<'_> { let function = bound_method.function(self.db); let self_instance = bound_method.self_instance(self.db); let self_instance_specialization = match self_instance { - Type::Instance(InstanceType::Class(ClassType::Generic(alias))) => { - Some(alias.specialization(self.db)) - } + Type::Instance(InstanceType { + class: ClassType::Generic(alias), + }) => Some(alias.specialization(self.db)), _ => None, }; let specialization = match function.specialization(self.db) { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 2141e15798781..f28df98779dee 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -991,7 +991,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::Instance(instance) if matches!( - instance.class(self.db()).known(self.db()), + instance.class.known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) ) => {} _ => return false, @@ -2891,10 +2891,7 @@ impl<'db> TypeInferenceBuilder<'db> { // Handle various singletons. if let Type::Instance(instance) = declared_ty.inner_type() { - if instance - .class(self.db()) - .is_known(self.db(), KnownClass::SpecialForm) - { + if instance.class.is_known(self.db(), KnownClass::SpecialForm) { if let Some(name_expr) = target.as_name_expr() { if let Some(known_instance) = KnownInstanceType::try_from_file_and_name( self.db(), @@ -5553,9 +5550,7 @@ impl<'db> TypeInferenceBuilder<'db> { range, ), (Type::Tuple(_), Type::Instance(instance)) - if instance - .class(self.db()) - .is_known(self.db(), KnownClass::VersionInfo) => + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( left, @@ -5565,9 +5560,7 @@ impl<'db> TypeInferenceBuilder<'db> { ) } (Type::Instance(instance), Type::Tuple(_)) - if instance - .class(self.db()) - .is_known(self.db(), KnownClass::VersionInfo) => + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( Type::version_info_tuple(self.db()), @@ -5950,16 +5943,12 @@ impl<'db> TypeInferenceBuilder<'db> { ( Type::Instance(instance), Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_), - ) if instance - .class(self.db()) - .is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_subscript_expression_types( + ) if instance.class.is_known(self.db(), KnownClass::VersionInfo) => self + .infer_subscript_expression_types( value_node, Type::version_info_tuple(self.db()), slice_ty, - ) - } + ), // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => { @@ -6235,9 +6224,7 @@ impl<'db> TypeInferenceBuilder<'db> { }, Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), Some(Type::Instance(instance)) - if instance - .class(self.db()) - .is_known(self.db(), KnownClass::NoneType) => + if instance.class.is_known(self.db(), KnownClass::NoneType) => { SliceArg::Arg(None) } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 9ae2df94db77f..c3f8177266313 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -108,21 +108,16 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( } } } + (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, - - (Type::Instance(InstanceType::Class(left)), Type::Instance(InstanceType::Class(right))) => { - left.cmp(right) - } - (Type::Instance(InstanceType::Class(_)), _) => Ordering::Less, - (_, Type::Instance(InstanceType::Class(_))) => Ordering::Greater, - ( - Type::Instance(InstanceType::UninitializedGenericClass(left)), - Type::Instance(InstanceType::UninitializedGenericClass(right)), + Type::Instance(InstanceType { class: left }), + Type::Instance(InstanceType { class: right }), ) => left.cmp(right), - (Type::Instance(InstanceType::UninitializedGenericClass(_)), _) => Ordering::Less, - (_, Type::Instance(InstanceType::UninitializedGenericClass(_))) => Ordering::Greater, + + (Type::Instance(_), _) => Ordering::Less, + (_, Type::Instance(_)) => Ordering::Greater, (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), (Type::TypeVar(_), _) => Ordering::Less, From 836d02cbbed6f56a78358880aba82d277e4cd833 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 10 Apr 2025 21:13:28 -0400 Subject: [PATCH 090/100] Remove unneeded helper method --- crates/red_knot_python_semantic/src/types.rs | 4 ---- crates/red_knot_python_semantic/src/types/generics.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 607032d68e95d..55bb5fb9a15d4 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4652,10 +4652,6 @@ impl<'db> TypeVarInstance<'db> { None } } - - pub(crate) fn default_type(self, db: &'db dyn Db) -> Type<'db> { - self.default_ty(db).unwrap_or(Type::unknown()) - } } #[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)] diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index a79b8ce9e8e0a..3182648580fec 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -183,7 +183,7 @@ impl<'db> SpecializationBuilder<'db> { self.types .remove(variable) .map(UnionBuilder::build) - .unwrap_or(variable.default_type(self.db)) + .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) }) .collect(); Specialization::new(self.db, self.generic_context, types) From 0744540d64a0d365e89de694c9b7bdd94169e6ed Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 11 Apr 2025 11:20:18 -0400 Subject: [PATCH 091/100] Add comment explaining the identity specialization --- crates/red_knot_python_semantic/src/types.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 55bb5fb9a15d4..ad078ac35fcd5 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3763,8 +3763,19 @@ impl<'db> Type<'db> { } }); + // Construct an instance type that we can use to look up the `__init__` instance method. + // This performs the same logic as `Type::to_instance`, except for generic class literals. // TODO: we should use the actual return type of `__new__` to determine the instance type let init_ty = match self { + // This is the only match arm that is different from `Type::to_instance`. Instead of + // using the _default_ specialization, we use the _identity_ specialization, which maps + // each of the class's generic typevars to itself. The end result is a generic alias + // that still has the typevars in it, which we need to be able to infer a + // specialization when we call the `__init__` method. This is how we simulate a + // "partially initialized" object, where we don't yet know the actual specialization + // (so we simulate that with the identity specialization), while still being able to do + // an _instance_ lookup of `__init__` (which would apply the default specialization if + // we were to perform it directly on the non-specialized generic class literal). Type::ClassLiteral(generic @ ClassLiteralType::Generic(_)) => { Type::instance(generic.identity_specialization(db)) } From d3067b01791ad44f49e389672120d542a5b34623 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 11 Apr 2025 11:22:46 -0400 Subject: [PATCH 092/100] Revert unneeded policy change --- crates/red_knot_python_semantic/src/types.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e16511fd87695..70f0081f66c62 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3829,12 +3829,7 @@ impl<'db> Type<'db> { .symbol .is_unbound() { - Some(init_ty.try_call_dunder_with_policy( - db, - "__init__", - &mut argument_types, - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - )) + Some(init_ty.try_call_dunder(db, "__init__", argument_types)) } else { None }; From 4823f43d22ccdc5318e569085aa064bd6a4ee408 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Apr 2025 16:21:08 -0400 Subject: [PATCH 093/100] Remove TODOs --- .../resources/mdtest/generics/functions.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index 13fdb5c8cd769..c1e36fa0a0e12 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -43,10 +43,6 @@ def absurd[T]() -> T: If the type of a generic function parameter is a typevar, then we can infer what type that typevar is bound to at each call site. -TODO: Note that some of the TODO revealed types have two options, since we haven't decided yet -whether we want to infer a more specific `Literal` type where possible, or use heuristics to weaken -the inferred type to e.g. `int`. - ```py def f[T](x: T) -> T: return x @@ -78,7 +74,6 @@ in the function. ```py def good_param[T: int](x: T) -> None: - # TODO: revealed: T & int reveal_type(x) # revealed: T ``` @@ -147,10 +142,7 @@ parameters simultaneously. def two_params[T](x: T, y: T) -> T: return x -# TODO: revealed: str reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] - -# TODO: revealed: str | int reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] ``` @@ -182,10 +174,7 @@ reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]: return y -# TODO: revealed: tuple[str, int] reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] - -# TODO: revealed: tuple[str, int] reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] ``` @@ -201,9 +190,6 @@ def f[T](x: T) -> tuple[T, int]: def g[T](x: T) -> T | None: return x -# TODO: revealed: tuple[str | None, int] reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] - -# TODO: revealed: tuple[str, int] | None reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` From 87f3f621fa27d071669bf2268e5eda5c5d2346f8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Apr 2025 16:23:36 -0400 Subject: [PATCH 094/100] Add TODO about nested contexts --- crates/red_knot_python_semantic/src/types/generics.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 3182648580fec..831dabbf02815 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -10,6 +10,9 @@ use crate::types::{ use crate::Db; /// A list of formal type variables for a generic function, class, or type alias. +/// +/// TODO: Handle nested generic contexts better, with actual parent links to the lexically +/// containing context. #[salsa::tracked(debug)] pub struct GenericContext<'db> { #[return_ref] @@ -110,6 +113,9 @@ impl<'db> GenericContext<'db> { } /// An assignment of a specific type to each type variable in a generic scope. +/// +/// TODO: Handle nested specializations better, with actual parent links to the specialization of +/// the lexically containing context. #[salsa::tracked(debug)] pub struct Specialization<'db> { pub(crate) generic_context: GenericContext<'db>, From beed9627b063812bb37d94fd4ff5374c352b5046 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Apr 2025 16:26:10 -0400 Subject: [PATCH 095/100] Add comment about potential class method inference --- crates/red_knot_python_semantic/src/types/class.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 85e7f19762dc0..e36f7d3c02cc0 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -296,6 +296,13 @@ impl<'db> ClassType<'db> { // specially: they inherit the generic context of their class. That lets us treat them // as generic functions when constructing the class, and infer the specialization of // the class from the arguments that are passed in. + // + // We might decide to handle other class methods the same way, having them inherit the + // class's generic context, and performing type inference on calls to them to determine + // the specialization of the class. If we do that, we would update this to also apply + // to any method with a `@classmethod` decorator. (`__init__` would remain a special + // case, since it's an _instance_ method where we don't yet know the generic class's + // specialization.) match (self, specialized, name) { ( ClassType::Generic(alias), From 94bf3f64cea9d3dc8a8dd6b6211088e27d7ec5b6 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Apr 2025 16:35:46 -0400 Subject: [PATCH 096/100] Real function body --- .../resources/mdtest/generics/functions.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index c1e36fa0a0e12..7fe3cf9ef0644 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -150,9 +150,10 @@ When one of the parameters is a union, we attempt to find the smallest specializ all of the constraints. ```py -# TODO: make this return list[T], so that we can write a correct body -# error: [invalid-return-type] -def union_param[T](x: T | None) -> T: ... +def union_param[T](x: T | None) -> T: + if x is None: + raise ValueError + return x reveal_type(union_param("a")) # revealed: Literal["a"] reveal_type(union_param(1)) # revealed: Literal[1] From 8f43e302c59a229be396e4b648bae9530506c62d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 16 Apr 2025 11:05:01 -0400 Subject: [PATCH 097/100] Remove unneeded special case lookup logic --- .../src/types/class.rs | 32 +++++++------------ .../src/types/slots.rs | 3 +- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 85e7f19762dc0..954eca4275226 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -289,24 +289,10 @@ impl<'db> ClassType<'db> { /// directly. Use [`ClassType::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - let (class_literal, _) = self.class_literal(db); - class_literal.own_class_member(db, name).map_type(|ty| { - let specialized = self.specialize_type(db, ty); - // The `__new__` and `__init__` members of a non-specialized generic class are handled - // specially: they inherit the generic context of their class. That lets us treat them - // as generic functions when constructing the class, and infer the specialization of - // the class from the arguments that are passed in. - match (self, specialized, name) { - ( - ClassType::Generic(alias), - Type::FunctionLiteral(function), - "__new__" | "__init__", - ) => Type::FunctionLiteral( - function.with_generic_context(db, alias.origin(db).generic_context(db)), - ), - _ => specialized, - } - }) + let (class_literal, specialization) = self.class_literal(db); + class_literal + .own_class_member(db, specialization, name) + .map_type(|ty| self.specialize_type(db, ty)) } /// Returns the `name` attribute of an instance of this class. @@ -820,17 +806,23 @@ impl<'db> ClassLiteralType<'db> { /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope /// directly. Use [`ClassLiteralType::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(super) fn own_class_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> SymbolAndQualifiers<'db> { let body_scope = self.body_scope(db); class_symbol(db, body_scope, name).map_type(|ty| { // The `__new__` and `__init__` members of a non-specialized generic class are handled // specially: they inherit the generic context of their class. That lets us treat them // as generic functions when constructing the class, and infer the specialization of // the class from the arguments that are passed in. - match (self, ty, name) { + match (self, ty, specialization, name) { ( ClassLiteralType::Generic(origin), Type::FunctionLiteral(function), + Some(_), "__new__" | "__init__", ) => Type::FunctionLiteral( function.with_generic_context(db, origin.generic_context(db)), diff --git a/crates/red_knot_python_semantic/src/types/slots.rs b/crates/red_knot_python_semantic/src/types/slots.rs index 2b265372ad16d..b86f2e2fa3b12 100644 --- a/crates/red_knot_python_semantic/src/types/slots.rs +++ b/crates/red_knot_python_semantic/src/types/slots.rs @@ -24,7 +24,8 @@ enum SlotsKind { impl SlotsKind { fn from(db: &dyn Db, base: ClassLiteralType) -> Self { - let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__").symbol else { + let Symbol::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").symbol + else { return Self::NotSpecified; }; From f38b43ea9cf9d2b5d1583e2f4b9f440094b4993c Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 16 Apr 2025 11:48:44 -0400 Subject: [PATCH 098/100] Add tests for new and init --- .../resources/mdtest/generics/classes.md | 66 +++++++++++++++++-- crates/red_knot_python_semantic/src/types.rs | 62 +++++++++-------- .../src/types/class.rs | 13 ---- .../src/types/generics.rs | 20 ++++++ 4 files changed, 114 insertions(+), 47 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 37f3e2f3b2f7a..b68960da2f1f8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -149,22 +149,76 @@ If a typevar does not provide a default, we use `Unknown`: reveal_type(C()) # revealed: C[Unknown] ``` +## Inferring generic class parameters from constructors + If the type of a constructor parameter is a class typevar, we can use that to infer the type -parameter: +parameter. The types inferred from a type context and from a constructor parameter must be +consistent with each other. + +## `__new__` only ```py -class E[T]: +class C[T]: + def __new__(cls, x: T) -> "C"[T]: + return object.__new__(cls) + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# TODO: error: [invalid-argument-type] +wrong_innards: C[int] = C("five") +``` + +## `__init__` only + +```py +class C[T]: def __init__(self, x: T) -> None: ... -reveal_type(E(1)) # revealed: E[Literal[1]] +reveal_type(C(1)) # revealed: C[Literal[1]] + +# TODO: error: [invalid-argument-type] +wrong_innards: C[int] = C("five") ``` -The types inferred from a type context and from a constructor parameter must be consistent with each -other: +## Identical `__new__` and `__init__` signatures ```py +class C[T]: + def __new__(cls, x: T) -> "C"[T]: + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# TODO: error: [invalid-argument-type] +wrong_innards: C[int] = C("five") +``` + +## Compatible `__new__` and `__init__` signatures + +```py +class C[T]: + def __new__(cls, *args, **kwargs) -> "C"[T]: + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# TODO: error: [invalid-argument-type] +wrong_innards: C[int] = C("five") + +class D[T]: + def __new__(cls, x: T) -> "D"[T]: + return object.__new__(cls) + + def __init__(self, *args, **kwargs) -> None: ... + +reveal_type(D(1)) # revealed: D[Literal[1]] + # TODO: error: [invalid-argument-type] -wrong_innards: E[int] = E("five") +wrong_innards: D[int] = D("five") ``` ## Generic subclass diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 70f0081f66c62..50ca6c7f8e173 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3751,10 +3751,22 @@ impl<'db> Type<'db> { )); // If we are trying to construct a non-specialized generic class, we should use the - // constructor parameters to try to infer the class specialization. - let generic_origin = match self { - Type::ClassLiteral(ClassLiteralType::Generic(generic)) => Some(generic), - _ => None, + // constructor parameters to try to infer the class specialization. To do this, we need to + // tweak our member lookup logic a bit. Normally, when looking up a class or instance + // member, we first apply the class's default specialization, and apply that specialization + // to the type of the member. To infer a specialization from the argument types, we need to + // have the class's typevars still in the method signature when we attempt to call it. To + // do this, we instead use the _identity_ specialization, which maps each of the class's + // generic typevars to itself. + let (generic_origin, self_type) = match self { + Type::ClassLiteral(ClassLiteralType::Generic(generic)) => { + let specialization = generic.generic_context(db).identity_specialization(db); + ( + Some(generic), + Type::GenericAlias(GenericAlias::new(db, generic, specialization)), + ) + } + _ => (None, self), }; // As of now we do not model custom `__call__` on meta-classes, so the code below @@ -3781,8 +3793,8 @@ impl<'db> Type<'db> { // An alternative might be to not skip `object.__new__` but instead mark it such that it's // easy to check if that's the one we found? // Note that `__new__` is a static method, so we must inject the `cls` argument. - let new_call_outcome = argument_types.with_self(Some(self), |argument_types| { - let result = self.try_call_dunder_with_policy( + let new_call_outcome = argument_types.with_self(Some(self_type), |argument_types| { + let result = self_type.try_call_dunder_with_policy( db, "__new__", argument_types, @@ -3798,26 +3810,9 @@ impl<'db> Type<'db> { // Construct an instance type that we can use to look up the `__init__` instance method. // This performs the same logic as `Type::to_instance`, except for generic class literals. // TODO: we should use the actual return type of `__new__` to determine the instance type - let init_ty = match self { - // This is the only match arm that is different from `Type::to_instance`. Instead of - // using the _default_ specialization, we use the _identity_ specialization, which maps - // each of the class's generic typevars to itself. The end result is a generic alias - // that still has the typevars in it, which we need to be able to infer a - // specialization when we call the `__init__` method. This is how we simulate a - // "partially initialized" object, where we don't yet know the actual specialization - // (so we simulate that with the identity specialization), while still being able to do - // an _instance_ lookup of `__init__` (which would apply the default specialization if - // we were to perform it directly on the non-specialized generic class literal). - Type::ClassLiteral(generic @ ClassLiteralType::Generic(_)) => { - Type::instance(generic.identity_specialization(db)) - } - Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { - Type::instance(ClassType::NonGeneric(non_generic)) - } - Type::GenericAlias(generic) => Type::instance(ClassType::Generic(generic)), - Type::SubclassOf(subclass_of) => subclass_of.to_instance(), - _ => panic!("type should be constructible"), - }; + let init_ty = self_type + .to_instance(db) + .expect("type should be convertible to instance type"); let init_call_outcome = if new_call_outcome.is_none() || !init_ty @@ -3834,9 +3829,11 @@ impl<'db> Type<'db> { None }; + // Note that we use `self` here, not `self_type`, so that if constructor argument inference + // fails, we fail back to the default specialization. let instance_ty = self .to_instance(db) - .expect("Class literal, generic alias, and subclass-of types should always be convertible to instance type"); + .expect("type should be convertible to instance type"); match (generic_origin, new_call_outcome, init_call_outcome) { // All calls are successful or not called at all @@ -3857,7 +3854,16 @@ impl<'db> Type<'db> { .and_then(Bindings::single_element) .and_then(CallableBinding::matching_overload) .and_then(|(_, binding)| binding.specialization()); - let specialized = (new_specialization.or(init_specialization)) + let specialization = match (new_specialization, init_specialization) { + (None, None) => None, + (Some(specialization), None) | (None, Some(specialization)) => { + Some(specialization) + } + (Some(new_specialization), Some(init_specialization)) => { + Some(new_specialization.combine(db, init_specialization)) + } + }; + let specialized = specialization .map(|specialization| { Type::instance(ClassType::Generic(GenericAlias::new( db, diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 954eca4275226..4ec8ed0a9a549 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -415,19 +415,6 @@ impl<'db> ClassLiteralType<'db> { } } - /// Returns the identity specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// applies the identity specialization to the class's typevars. - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), - Self::Generic(generic) => { - let specialization = generic.generic_context(db).identity_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) - } - } - } - /// Returns the unknown specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 3182648580fec..f6403ddeec859 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -140,6 +140,26 @@ impl<'db> Specialization<'db> { Specialization::new(db, self.generic_context(db), types) } + /// Combines two specializations of the same generic context. If either specialization maps a + /// typevar to `Type::Unknown`, the other specialization's mapping is used. If both map the + /// typevar to a known type, those types are unioned together. + /// + /// Panics if the two specializations are not for the same generic context. + pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { + let generic_context = self.generic_context(db); + assert!(other.generic_context(db) == generic_context); + let types = self + .types(db) + .into_iter() + .zip(other.types(db)) + .map(|(self_type, other_type)| match (self_type, other_type) { + (unknown, known) | (known, unknown) if unknown.is_unknown() => *known, + _ => UnionType::from_elements(db, [self_type, other_type]), + }) + .collect(); + Specialization::new(db, self.generic_context(db), types) + } + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { let types = self.types(db).iter().map(|ty| ty.normalized(db)).collect(); Self::new(db, self.generic_context(db), types) From 394c762e43addce0f1769343377ad88429ddbd74 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 16 Apr 2025 13:09:21 -0400 Subject: [PATCH 099/100] Add failing test case for generic __init__ --- .../resources/mdtest/generics/classes.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 b68960da2f1f8..85574c615c9e6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -221,6 +221,32 @@ reveal_type(D(1)) # revealed: D[Literal[1]] wrong_innards: D[int] = D("five") ``` +## `__init__` is itself generic + +TODO: These do not currently work yet, because we don't correctly model the nested generic contexts. + +```py +class C[T]: + def __init__[S](self, x: T, y: S) -> None: ... + +# TODO: no error +# TODO: revealed: C[Literal[1]] +# error: [invalid-argument-type] +reveal_type(C(1, 1)) # revealed: C[Unknown] +# TODO: no error +# TODO: revealed: C[Literal[1]] +# error: [invalid-argument-type] +reveal_type(C(1, "string")) # revealed: C[Unknown] +# TODO: no error +# TODO: revealed: C[Literal[1]] +# error: [invalid-argument-type] +reveal_type(C(1, True)) # revealed: C[Unknown] + +# TODO: error for the correct reason +# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`" +wrong_innards: C[int] = C("five", 1) +``` + ## Generic subclass When a generic subclass fills its superclass's type parameter with one of its own, the actual types From 523ddcb9687fc8ded2c7ce71039cc6f0bd9f5dfa Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 16 Apr 2025 14:57:39 -0400 Subject: [PATCH 100/100] clean up known class detection --- .../src/types/infer.rs | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 25e1943e45ee4..59248f9f7bc2c 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -82,13 +82,13 @@ use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ todo_type, CallDunderError, CallableSignature, CallableType, Class, ClassLiteralType, - DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, GenericAlias, GenericClass, - IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, - MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter, ParameterForm, Parameters, - Signature, Signatures, SliceLiteralType, StringLiteralType, SubclassOfType, Symbol, - SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, - TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, - UnionType, + ClassType, DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, GenericAlias, + GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, + KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter, + ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType, + SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, + TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; @@ -4188,35 +4188,36 @@ impl<'db> TypeInferenceBuilder<'db> { let callable_type = self.infer_expression(func); // For class literals we model the entire class instantiation logic, so it is handled - // in a separate function. - let class = match callable_type { - Type::ClassLiteral(_) | Type::GenericAlias(_) => Some(callable_type), - Type::SubclassOf(subclass) if matches!(subclass.subclass_of(), ClassBase::Class(_)) => { - Some(callable_type) - } - _ => None, + // in a separate function. For some known classes we have manual signatures defined and use + // the `try_call` path below. + // TODO: it should be possible to move these special cases into the `try_call_constructor` + // path instead, or even remove some entirely once we support overloads fully. + let (call_constructor, known_class) = match callable_type { + Type::ClassLiteral(class) => (true, class.known(self.db())), + Type::GenericAlias(generic) => (true, ClassType::Generic(generic).known(self.db())), + Type::SubclassOf(subclass) => ( + true, + subclass + .subclass_of() + .into_class() + .and_then(|class| class.known(self.db())), + ), + _ => (false, None), }; - if class.is_some_and(|class| { - // For some known classes we have manual signatures defined and use the `try_call` path - // below. TODO: it should be possible to move these special cases into the - // `try_call_constructor` path instead, or even remove some entirely once we support - // overloads fully. - match class { - Type::ClassLiteral(class) => class.known(self.db()).is_none_or(|class| { - !matches!( - class, - KnownClass::Bool - | KnownClass::Str - | KnownClass::Type - | KnownClass::Object - | KnownClass::Property - | KnownClass::Super - ) - }), - _ => true, - } - }) { + if call_constructor + && !matches!( + known_class, + Some( + KnownClass::Bool + | KnownClass::Str + | KnownClass::Type + | KnownClass::Object + | KnownClass::Property + | KnownClass::Super + ) + ) + { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; let call_argument_types = self.infer_argument_types(arguments, call_arguments, &argument_forms);