From 645ce7e5ec068be09289d0dad84a0b6c27730f30 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 8 Nov 2024 13:23:05 -0800 Subject: [PATCH] [red-knot] infer types for PEP695 typevars (#14182) ## Summary Create definitions and infer types for PEP 695 type variables. This just gives us the type of the type variable itself (the type of `T` as a runtime object in the body of `def f[T](): ...`), with special handling for its attributes `__name__`, `__bound__`, `__constraints__`, and `__default__`. Mostly the support for these attributes exists because it is easy to implement and allows testing that we are internally representing the typevar correctly. This PR doesn't yet have support for interpreting a typevar as a type annotation, which is of course the primary use of a typevar. But the information we store in the typevar's type in this PR gives us everything we need to handle it correctly in a future PR when the typevar appears in an annotation. ## Test Plan Added mdtest. --- .../resources/mdtest/generics.md | 63 +++++- .../src/semantic_index/builder.rs | 5 + .../src/semantic_index/definition.rs | 59 +++++- crates/red_knot_python_semantic/src/types.rs | 200 +++++++++++++++--- .../src/types/display.rs | 5 + .../src/types/infer.rs | 139 +++++++++--- .../red_knot_python_semantic/src/types/mro.rs | 1 + .../test/corpus/27_func_generic_constraint.py | 3 + 8 files changed, 395 insertions(+), 80 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics.md b/crates/red_knot_python_semantic/resources/mdtest/generics.md index ff9f5ff8d1..d4ccce2ae7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics.md @@ -6,13 +6,9 @@ Basic PEP 695 generics ```py class MyBox[T]: - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" data: T box_model_number = 695 - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" def __init__(self, data: T): self.data = data @@ -31,17 +27,12 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695] ```py class MyBox[T]: - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" data: T - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" def __init__(self, data: T): self.data = data -# TODO not error on the subscripting or the use of type param -# error: [unresolved-reference] "Name `T` used when not defined" +# TODO not error on the subscripting # error: [non-subscriptable] class MySecureBox[T](MyBox[T]): ... @@ -66,3 +57,55 @@ class S[T](Seq[S]): ... # error: [non-subscriptable] reveal_type(S) # revealed: Literal[S] ``` + +## Type params + +A PEP695 type variable defines a value of type `typing.TypeVar` with attributes `__name__`, +`__bounds__`, `__constraints__`, and `__default__` (the latter three all lazily evaluated): + +```py +def f[T, U: A, V: (A, B), W = A, X: A = A1](): + reveal_type(T) # revealed: TypeVar + reveal_type(T.__name__) # revealed: Literal["T"] + reveal_type(T.__bound__) # revealed: None + reveal_type(T.__constraints__) # revealed: tuple[()] + reveal_type(T.__default__) # revealed: NoDefault + + reveal_type(U) # revealed: TypeVar + reveal_type(U.__name__) # revealed: Literal["U"] + reveal_type(U.__bound__) # revealed: type[A] + reveal_type(U.__constraints__) # revealed: tuple[()] + reveal_type(U.__default__) # revealed: NoDefault + + reveal_type(V) # revealed: TypeVar + reveal_type(V.__name__) # revealed: Literal["V"] + reveal_type(V.__bound__) # revealed: None + reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]] + reveal_type(V.__default__) # revealed: NoDefault + + reveal_type(W) # revealed: TypeVar + reveal_type(W.__name__) # revealed: Literal["W"] + reveal_type(W.__bound__) # revealed: None + reveal_type(W.__constraints__) # revealed: tuple[()] + reveal_type(W.__default__) # revealed: type[A] + + reveal_type(X) # revealed: TypeVar + reveal_type(X.__name__) # revealed: Literal["X"] + reveal_type(X.__bound__) # revealed: type[A] + reveal_type(X.__constraints__) # revealed: tuple[()] + reveal_type(X.__default__) # revealed: type[A1] + +class A: ... +class B: ... +class A1(A): ... +``` + +## Minimum two constraints + +A typevar with less than two constraints emits a diagnostic and is treated as unconstrained: + +```py +# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types" +def f[T: (int,)](): + reveal_type(T.__constraints__) # revealed: tuple[()] +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index ba50e16148..fda1f07cbc 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -373,6 +373,11 @@ impl<'db> SemanticIndexBuilder<'db> { if let Some(default) = default { self.visit_expr(default); } + match type_param { + ast::TypeParam::TypeVar(node) => self.add_definition(symbol, node), + ast::TypeParam::ParamSpec(node) => self.add_definition(symbol, node), + ast::TypeParam::TypeVarTuple(node) => self.add_definition(symbol, node), + }; } } diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index d165e521e8..11822e5d75 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -92,6 +92,9 @@ pub(crate) enum DefinitionNodeRef<'a> { WithItem(WithItemDefinitionNodeRef<'a>), MatchPattern(MatchPatternDefinitionNodeRef<'a>), ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>), + TypeVar(&'a ast::TypeParamTypeVar), + ParamSpec(&'a ast::TypeParamParamSpec), + TypeVarTuple(&'a ast::TypeParamTypeVarTuple), } impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> { @@ -130,6 +133,24 @@ impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> { } } +impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> { + fn from(value: &'a ast::TypeParamTypeVar) -> Self { + Self::TypeVar(value) + } +} + +impl<'a> From<&'a ast::TypeParamParamSpec> for DefinitionNodeRef<'a> { + fn from(value: &'a ast::TypeParamParamSpec) -> Self { + Self::ParamSpec(value) + } +} + +impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> { + fn from(value: &'a ast::TypeParamTypeVarTuple) -> Self { + Self::TypeVarTuple(value) + } +} + impl<'a> From> for DefinitionNodeRef<'a> { fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self { Self::ImportFrom(node_ref) @@ -317,6 +338,15 @@ impl<'db> DefinitionNodeRef<'db> { handler: AstNodeRef::new(parsed, handler), is_star, }), + DefinitionNodeRef::TypeVar(node) => { + DefinitionKind::TypeVar(AstNodeRef::new(parsed, node)) + } + DefinitionNodeRef::ParamSpec(node) => { + DefinitionKind::ParamSpec(AstNodeRef::new(parsed, node)) + } + DefinitionNodeRef::TypeVarTuple(node) => { + DefinitionKind::TypeVarTuple(AstNodeRef::new(parsed, node)) + } } } @@ -356,6 +386,9 @@ impl<'db> DefinitionNodeRef<'db> { identifier.into() } Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(), + Self::TypeVar(node) => node.into(), + Self::ParamSpec(node) => node.into(), + Self::TypeVarTuple(node) => node.into(), } } } @@ -412,6 +445,9 @@ pub enum DefinitionKind<'db> { WithItem(WithItemDefinitionKind), MatchPattern(MatchPatternDefinitionKind), ExceptHandler(ExceptHandlerDefinitionKind), + TypeVar(AstNodeRef), + ParamSpec(AstNodeRef), + TypeVarTuple(AstNodeRef), } impl DefinitionKind<'_> { @@ -421,7 +457,10 @@ impl DefinitionKind<'_> { DefinitionKind::Function(_) | DefinitionKind::Class(_) | DefinitionKind::Import(_) - | DefinitionKind::ImportFrom(_) => DefinitionCategory::DeclarationAndBinding, + | DefinitionKind::ImportFrom(_) + | DefinitionKind::TypeVar(_) + | DefinitionKind::ParamSpec(_) + | DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding, // a parameter always binds a value, but is only a declaration if annotated DefinitionKind::Parameter(parameter) => { if parameter.annotation.is_some() { @@ -696,3 +735,21 @@ impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey { Self(NodeKey::from_node(handler)) } } + +impl From<&ast::TypeParamTypeVar> for DefinitionNodeKey { + fn from(value: &ast::TypeParamTypeVar) -> Self { + Self(NodeKey::from_node(value)) + } +} + +impl From<&ast::TypeParamParamSpec> for DefinitionNodeKey { + fn from(value: &ast::TypeParamParamSpec) -> Self { + Self(NodeKey::from_node(value)) + } +} + +impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey { + fn from(value: &ast::TypeParamTypeVarTuple) -> Self { + Self(NodeKey::from_node(value)) + } +} diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 46a4d97475..947ccb801c 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -307,7 +307,7 @@ fn declarations_ty<'db>( } /// Representation of a type: a set of possible values at runtime. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub enum Type<'db> { /// The dynamic type: a statically unknown set of values Any, @@ -336,7 +336,7 @@ pub enum Type<'db> { /// The set of Python objects with the given class in their __class__'s method resolution order Instance(InstanceType<'db>), /// A single Python object that requires special treatment in the type system - KnownInstance(KnownInstanceType), + KnownInstance(KnownInstanceType<'db>), /// The set of objects in any of the types in the union Union(UnionType<'db>), /// The set of objects in all of the types in the intersection @@ -657,17 +657,23 @@ impl<'db> Type<'db> { // TODO: Once we have support for final classes, we can establish that // `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`. - // TODO: The following is a workaround that is required to unify the two different - // versions of `NoneType` in typeshed. This should not be required anymore once we - // understand `sys.version_info` branches. + // TODO: The following is a workaround that is required to unify the two different versions + // of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once + // we understand `sys.version_info` branches. self == other || matches!((self, other), ( Type::Instance(InstanceType { class: self_class }), Type::Instance(InstanceType { class: target_class }) ) - if self_class.is_known(db, KnownClass::NoneType) && - target_class.is_known(db, KnownClass::NoneType)) + if ( + self_class.is_known(db, KnownClass::NoneType) && + target_class.is_known(db, KnownClass::NoneType) + ) || ( + self_class.is_known(db, KnownClass::NoDefaultType) && + target_class.is_known(db, KnownClass::NoDefaultType) + ) + ) } /// Return true if this type and `other` have no common elements. @@ -907,8 +913,11 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(..) | Type::KnownInstance(..) => true, Type::Instance(InstanceType { class }) => { - // TODO some more instance types can be singleton types (EllipsisType, NotImplementedType) - matches!(class.known(db), Some(KnownClass::NoneType)) + if let Some(known_class) = class.known(db) { + known_class.is_singleton() + } else { + false + } } Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -961,7 +970,7 @@ impl<'db> Type<'db> { .all(|elem| elem.is_single_valued(db)), Type::Instance(InstanceType { class }) => match class.known(db) { - Some(KnownClass::NoneType) => true, + Some(KnownClass::NoneType | KnownClass::NoDefaultType) => true, Some( KnownClass::Bool | KnownClass::Object @@ -978,7 +987,8 @@ impl<'db> Type<'db> { | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType - | KnownClass::SpecialForm, + | KnownClass::SpecialForm + | KnownClass::TypeVar, ) => false, None => false, }, @@ -1049,9 +1059,7 @@ impl<'db> Type<'db> { } Type::ClassLiteral(class_ty) => class_ty.member(db, name), Type::SubclassOf(subclass_of_ty) => subclass_of_ty.member(db, name), - Type::KnownInstance(known_instance) => { - known_instance.instance_fallback(db).member(db, name) - } + Type::KnownInstance(known_instance) => known_instance.member(db, name), Type::Instance(_) => { // TODO MRO? get_own_instance_member, get_instance_member Type::Todo.into() @@ -1442,7 +1450,7 @@ impl<'db> Type<'db> { Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), Type::StringLiteral(_) | Type::LiteralString => *self, Type::KnownInstance(known_instance) => { - Type::StringLiteral(StringLiteralType::new(db, known_instance.repr())) + Type::StringLiteral(StringLiteralType::new(db, known_instance.repr(db))) } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), @@ -1464,7 +1472,7 @@ impl<'db> Type<'db> { })), Type::LiteralString => Type::LiteralString, Type::KnownInstance(known_instance) => { - Type::StringLiteral(StringLiteralType::new(db, known_instance.repr())) + Type::StringLiteral(StringLiteralType::new(db, known_instance.repr(db))) } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), @@ -1514,7 +1522,10 @@ pub enum KnownClass { FunctionType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 + // Typing SpecialForm, + TypeVar, + NoDefaultType, } impl<'db> KnownClass { @@ -1537,6 +1548,8 @@ impl<'db> KnownClass { Self::FunctionType => "FunctionType", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", + Self::TypeVar => "TypeVar", + Self::NoDefaultType => "_NoDefaultType", } } @@ -1561,8 +1574,34 @@ impl<'db> KnownClass { Self::GenericAlias | Self::ModuleType | Self::FunctionType => { types_symbol(db, self.as_str()).unwrap_or_unknown() } - Self::SpecialForm => typing_symbol(db, self.as_str()).unwrap_or_unknown(), Self::NoneType => typeshed_symbol(db, self.as_str()).unwrap_or_unknown(), + Self::SpecialForm => typing_symbol(db, self.as_str()).unwrap_or_unknown(), + Self::TypeVar => typing_symbol(db, self.as_str()).unwrap_or_unknown(), + Self::NoDefaultType => typing_extensions_symbol(db, self.as_str()).unwrap_or_unknown(), + } + } + + fn is_singleton(self) -> bool { + // TODO there are other singleton types (EllipsisType, NotImplementedType) + match self { + Self::NoneType | Self::NoDefaultType => true, + Self::Bool + | Self::Object + | Self::Bytes + | Self::Tuple + | Self::Int + | Self::Float + | Self::Str + | Self::Set + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::SpecialForm + | Self::TypeVar => false, } } @@ -1597,6 +1636,7 @@ impl<'db> KnownClass { "ModuleType" => Some(Self::ModuleType), "FunctionType" => Some(Self::FunctionType), "_SpecialForm" => Some(Self::SpecialForm), + "_NoDefaultType" => Some(Self::NoDefaultType), _ => None, } } @@ -1621,7 +1661,7 @@ impl<'db> KnownClass { | Self::Slice => module.name() == "builtins", Self::GenericAlias | Self::ModuleType | Self::FunctionType => module.name() == "types", Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"), - Self::SpecialForm => { + Self::SpecialForm | Self::TypeVar | Self::NoDefaultType => { matches!(module.name().as_str(), "typing" | "typing_extensions") } } @@ -1629,17 +1669,20 @@ impl<'db> KnownClass { } /// Enumeration of specific runtime that are special enough to be considered their own type. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KnownInstanceType { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum KnownInstanceType<'db> { /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) Literal, + /// A single instance of `typing.TypeVar` + TypeVar(TypeVarInstance<'db>), // TODO: fill this enum out with more special forms, etc. } -impl KnownInstanceType { +impl<'db> KnownInstanceType<'db> { pub const fn as_str(self) -> &'static str { match self { KnownInstanceType::Literal => "Literal", + KnownInstanceType::TypeVar(_) => "TypeVar", } } @@ -1647,13 +1690,15 @@ impl KnownInstanceType { pub const fn bool(self) -> Truthiness { match self { Self::Literal => Truthiness::AlwaysTrue, + Self::TypeVar(_) => Truthiness::AlwaysTrue, } } /// Return the repr of the symbol at runtime - pub const fn repr(self) -> &'static str { + pub fn repr(self, db: &'db dyn Db) -> &'db str { match self { Self::Literal => "typing.Literal", + Self::TypeVar(typevar) => typevar.name(db), } } @@ -1661,6 +1706,7 @@ impl KnownInstanceType { pub const fn class(self) -> KnownClass { match self { Self::Literal => KnownClass::SpecialForm, + Self::TypeVar(_) => KnownClass::TypeVar, } } @@ -1682,6 +1728,93 @@ impl KnownInstanceType { _ => None, } } + + fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + match (self, name) { + (Self::TypeVar(typevar), "__name__") => Symbol::Type( + Type::StringLiteral(StringLiteralType::new(db, typevar.name(db).as_str())), + Boundness::Bound, + ), + (Self::TypeVar(typevar), "__bound__") => Symbol::Type( + typevar + .upper_bound(db) + .map(|ty| ty.to_meta_type(db)) + .unwrap_or(KnownClass::NoneType.to_instance(db)), + Boundness::Bound, + ), + (Self::TypeVar(typevar), "__constraints__") => Symbol::Type( + Type::Tuple(TupleType::new( + db, + typevar + .constraints(db) + .map(|constraints| { + constraints + .iter() + .map(|ty| ty.to_meta_type(db)) + .collect::>() + }) + .unwrap_or_else(|| std::iter::empty().collect::>()), + )), + Boundness::Bound, + ), + (Self::TypeVar(typevar), "__default__") => Symbol::Type( + typevar + .default_ty(db) + .map(|ty| ty.to_meta_type(db)) + .unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)), + Boundness::Bound, + ), + _ => self.instance_fallback(db).member(db, name), + } + } +} + +/// 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. +/// +/// 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 +/// different typevars: if used in the same scope, they may be bound to different types. +#[salsa::tracked] +pub struct TypeVarInstance<'db> { + /// The name of this TypeVar (e.g. `T`) + #[return_ref] + name: ast::name::Name, + + /// The upper bound or constraint on the type of this TypeVar + bound_or_constraints: Option>, + + /// The default type for this TypeVar + default_ty: Option>, +} + +impl<'db> TypeVarInstance<'db> { + pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { + if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { + Some(ty) + } else { + None + } + } + + pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&[Type<'db>]> { + if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { + Some(tuple.elements(db)) + } else { + None + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)] +pub enum TypeVarBoundOrConstraints<'db> { + UpperBound(Type<'db>), + Constraints(TupleType<'db>), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -2499,7 +2632,7 @@ impl SeenSet { } /// A singleton type representing a single class object at runtime. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct ClassLiteralType<'db> { class: Class<'db>, } @@ -2521,7 +2654,7 @@ impl<'db> From> for Type<'db> { } /// A type that represents `type[C]`, i.e. the class literal `C` and class literals that are subclasses of `C`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct SubclassOfType<'db> { class: Class<'db>, } @@ -2533,7 +2666,7 @@ impl<'db> SubclassOfType<'db> { } /// A type representing the set of runtime objects which are instances of a certain class. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] pub struct InstanceType<'db> { class: Class<'db>, } @@ -2746,9 +2879,10 @@ mod tests { // BuiltinInstance("str") corresponds to an instance of the builtin `str` class BuiltinInstance(&'static str), TypingInstance(&'static str), - KnownInstance(KnownInstanceType), + TypingLiteral, // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself BuiltinClassLiteral(&'static str), + KnownClassInstance(KnownClass), Union(Vec), Intersection { pos: Vec, neg: Vec }, Tuple(Vec), @@ -2769,8 +2903,9 @@ mod tests { Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())), Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db), Ty::TypingInstance(s) => typing_symbol(db, s).expect_type().to_instance(db), - Ty::KnownInstance(known_instance) => Type::KnownInstance(known_instance), + Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal), Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).expect_type(), + Ty::KnownClassInstance(known_class) => known_class.to_instance(db), Ty::Union(tys) => { UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) } @@ -2876,14 +3011,8 @@ mod tests { #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("str")], neg: vec![Ty::StringLiteral("foo")]}, Ty::Intersection{pos: vec![], neg: vec![Ty::IntLiteral(2)]})] #[test_case(Ty::BuiltinClassLiteral("int"), Ty::BuiltinClassLiteral("int"))] #[test_case(Ty::BuiltinClassLiteral("int"), Ty::BuiltinInstance("object"))] - #[test_case( - Ty::KnownInstance(KnownInstanceType::Literal), - Ty::TypingInstance("_SpecialForm") - )] - #[test_case( - Ty::KnownInstance(KnownInstanceType::Literal), - Ty::BuiltinInstance("object") - )] + #[test_case(Ty::TypingLiteral, Ty::TypingInstance("_SpecialForm"))] + #[test_case(Ty::TypingLiteral, Ty::BuiltinInstance("object"))] fn is_subtype_of(from: Ty, to: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); @@ -3126,6 +3255,7 @@ mod tests { #[test_case(Ty::None)] #[test_case(Ty::BooleanLiteral(true))] #[test_case(Ty::BooleanLiteral(false))] + #[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))] fn is_singleton(from: Ty) { let db = setup_db(); diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 6818fe12d6..5bcafd902e 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -71,6 +71,11 @@ impl Display for DisplayRepresentation<'_> { { f.write_str("None") } + Type::Instance(InstanceType { class }) + if class.is_known(self.db, KnownClass::NoDefaultType) => + { + f.write_str("NoDefault") + } // `[Type::Todo]`'s display should be explicit that is not a valid display of // any other type Type::Todo => f.write_str("@Todo"), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index f68f502e25..30e9965e73 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -59,7 +59,8 @@ use crate::types::{ Boundness, BytesLiteralType, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, StringLiteralType, - Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType, + Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, TypeVarBoundOrConstraints, + TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -642,6 +643,15 @@ impl<'db> TypeInferenceBuilder<'db> { DefinitionKind::ExceptHandler(except_handler_definition) => { self.infer_except_handler_definition(except_handler_definition, definition); } + DefinitionKind::TypeVar(node) => { + self.infer_typevar_definition(node, definition); + } + DefinitionKind::ParamSpec(node) => { + self.infer_paramspec_definition(node, definition); + } + DefinitionKind::TypeVarTuple(node) => { + self.infer_typevartuple_definition(node, definition); + } } } @@ -1352,6 +1362,82 @@ impl<'db> TypeInferenceBuilder<'db> { ); } + fn infer_typevar_definition( + &mut self, + node: &ast::TypeParamTypeVar, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVar { + range: _, + name, + bound, + default, + } = node; + let bound_or_constraint = match bound.as_deref() { + Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { + if elts.len() < 2 { + self.diagnostics.add( + expr.into(), + "invalid-typevar-constraints", + format_args!("TypeVar must have at least two constrained types"), + ); + self.infer_expression(expr); + None + } else { + let tuple = TupleType::new( + self.db, + elts.iter() + .map(|expr| self.infer_type_expression(expr)) + .collect::>(), + ); + let constraints = TypeVarBoundOrConstraints::Constraints(tuple); + self.store_expression_type(expr, Type::Tuple(tuple)); + Some(constraints) + } + } + Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound( + self.infer_type_expression(expr), + )), + None => None, + }; + let default_ty = self.infer_optional_type_expression(default.as_deref()); + let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + self.db, + name.id.clone(), + bound_or_constraint, + default_ty, + ))); + self.add_declaration_with_binding(node.into(), definition, ty, ty); + } + + fn infer_paramspec_definition( + &mut self, + node: &ast::TypeParamParamSpec, + definition: Definition<'db>, + ) { + let ast::TypeParamParamSpec { + range: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref()); + self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo); + } + + fn infer_typevartuple_definition( + &mut self, + node: &ast::TypeParamTypeVarTuple, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVarTuple { + range: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref()); + self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo); + } + fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { let ast::StmtMatch { range: _, @@ -2013,13 +2099,6 @@ impl<'db> TypeInferenceBuilder<'db> { expression.map(|expr| self.infer_expression(expr)) } - fn infer_optional_annotation_expression( - &mut self, - expr: Option<&ast::Expr>, - ) -> Option> { - expr.map(|expr| self.infer_annotation_expression(expr)) - } - #[track_caller] fn infer_expression(&mut self, expression: &ast::Expr) -> Type<'db> { debug_assert_eq!( @@ -3912,32 +3991,9 @@ impl<'db> TypeInferenceBuilder<'db> { } = type_parameters; for type_param in type_params { match type_param { - ast::TypeParam::TypeVar(typevar) => { - let ast::TypeParamTypeVar { - range: _, - name: _, - bound, - default, - } = typevar; - self.infer_optional_expression(bound.as_deref()); - self.infer_optional_expression(default.as_deref()); - } - ast::TypeParam::ParamSpec(param_spec) => { - let ast::TypeParamParamSpec { - range: _, - name: _, - default, - } = param_spec; - self.infer_optional_expression(default.as_deref()); - } - ast::TypeParam::TypeVarTuple(typevar_tuple) => { - let ast::TypeParamTypeVarTuple { - range: _, - name: _, - default, - } = typevar_tuple; - self.infer_optional_expression(default.as_deref()); - } + ast::TypeParam::TypeVar(node) => self.infer_definition(node), + ast::TypeParam::ParamSpec(node) => self.infer_definition(node), + ast::TypeParam::TypeVarTuple(node) => self.infer_definition(node), } } } @@ -3971,6 +4027,13 @@ impl<'db> TypeInferenceBuilder<'db> { self.store_expression_type(expression, annotation_ty); annotation_ty } + + fn infer_optional_annotation_expression( + &mut self, + expr: Option<&ast::Expr>, + ) -> Option> { + expr.map(|expr| self.infer_annotation_expression(expr)) + } } /// Type expressions @@ -4145,6 +4208,13 @@ impl<'db> TypeInferenceBuilder<'db> { ty } + fn infer_optional_type_expression( + &mut self, + opt_expression: Option<&ast::Expr>, + ) -> Option> { + opt_expression.map(|expr| self.infer_type_expression(expr)) + } + /// Given the slice of a `tuple[]` annotation, return the type that the annotation represents fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> { /// In most cases, if a subelement of the tuple is inferred as `Todo`, @@ -4262,6 +4332,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown } }, + KnownInstanceType::TypeVar(_) => Type::Todo, } } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index c30ad49a62..254a6f03fd 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -381,6 +381,7 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) => None, Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::Literal => None, + KnownInstanceType::TypeVar(_) => None, }, } } diff --git a/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py b/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py index 8f5867f38c..763eea4827 100644 --- a/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py +++ b/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py @@ -1,2 +1,5 @@ def foo[T: (str, bytes)](x: T) -> T: ... + +def bar[T: (str,)](x: T) -> T: + ...