From 95294e657ce7e884162ac1fd3d26678593ff1fab Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 7 Jan 2025 12:53:07 +0000 Subject: [PATCH] [red-knot] Eagerly normalize `type[]` types (#15272) Co-authored-by: Carl Meyer --- .../resources/mdtest/narrow/issubclass.md | 2 +- .../resources/mdtest/type_of/basic.md | 22 ++ .../resources/mdtest/type_of/dynamic.md | 5 +- crates/red_knot_python_semantic/src/types.rs | 244 +++++++----------- .../src/types/class_base.rs | 7 + .../src/types/display.rs | 18 +- .../src/types/infer.rs | 9 +- .../src/types/narrow.rs | 20 +- .../src/types/subclass_of.rs | 92 +++++++ 9 files changed, 245 insertions(+), 174 deletions(-) create mode 100644 crates/red_knot_python_semantic/src/types/subclass_of.rs diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md index d539a28a02742..7d5697e4f7f45 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md @@ -90,7 +90,7 @@ def _(t: type[object]): if issubclass(t, B): reveal_type(t) # revealed: type[A] & type[B] else: - reveal_type(t) # revealed: type[object] & ~type[A] + reveal_type(t) # revealed: type & ~type[A] ``` ### Handling of `None` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md index 192b94b08c822..2a7d51c62b025 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md @@ -142,3 +142,25 @@ class Foo(type[int]): ... # TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]] reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] ``` + +## `@final` classes + +`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is +used as the type argument. This applies to standard-library classes and user-defined classes: + +```toml +[environment] +python-version = "3.10" +``` + +```py +from types import EllipsisType +from typing import final + +@final +class Foo: ... + +def _(x: type[Foo], y: type[EllipsisType]): + reveal_type(x) # revealed: Literal[Foo] + reveal_type(y) # revealed: Literal[EllipsisType] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md index 9bc257ddaee56..4e60f63bde036 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md @@ -47,9 +47,8 @@ x: type = A() # error: [invalid-assignment] ```py def f(x: type[object]): - reveal_type(x) # revealed: type[object] - # TODO: bound method types - reveal_type(x.__repr__) # revealed: Literal[__repr__] + reveal_type(x) # revealed: type + reveal_type(x.__repr__) # revealed: @Todo(instance attributes) class A: ... diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f3356b9320511..6cbf49909c90d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -17,6 +17,7 @@ pub(crate) use self::infer::{ }; pub use self::narrow::KnownConstraintFunction; pub(crate) use self::signatures::Signature; +pub use self::subclass_of::SubclassOfType; use crate::module_name::ModuleName; use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; use crate::semantic_index::ast_ids::HasScopedExpressionId; @@ -49,6 +50,7 @@ mod narrow; mod signatures; mod slots; mod string_annotation; +mod subclass_of; mod unpacker; #[cfg(test)] @@ -693,14 +695,6 @@ impl<'db> Type<'db> { Self::Instance(InstanceType { class }) } - pub const fn subclass_of(class: Class<'db>) -> Self { - Self::subclass_of_base(ClassBase::Class(class)) - } - - pub const fn subclass_of_base(base: ClassBase<'db>) -> Self { - Self::SubclassOf(SubclassOfType { base }) - } - pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { Self::StringLiteral(StringLiteralType::new(db, string)) } @@ -887,16 +881,31 @@ impl<'db> Type<'db> { // as that type is equivalent to `type[Any, ...]` (and therefore not a fully static type). (Type::Tuple(_), _) => KnownClass::Tuple.to_instance(db).is_subtype_of(db, target), - // `Type::ClassLiteral` always delegates to `Type::SubclassOf`: - (Type::ClassLiteral(ClassLiteralType { class }), _) => { - Type::subclass_of(class).is_subtype_of(db, target) - } + // `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 + .subclass_of() + .into_class() + .is_some_and(|target_class| class.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)) => { self_subclass_ty.is_subtype_of(db, target_subclass_ty) } + // `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: self_class }), + Type::Instance(InstanceType { + class: target_class, + }), + ) => self_class.is_instance_of(db, target_class), + // `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` // is an instance of `type`, and so all possible subclasses of `str` will also be instances of `type`. @@ -904,16 +913,19 @@ impl<'db> Type<'db> { // Similarly `type[enum.Enum]` is a subtype of `enum.EnumMeta` because `enum.Enum` // is an instance of `enum.EnumMeta`. ( - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(self_class), - }), + Type::SubclassOf(subclass_of_ty), Type::Instance(InstanceType { class: target_class, }), - ) => self_class.is_instance_of(db, target_class), + ) => subclass_of_ty + .subclass_of() + .into_class() + .is_some_and(|subclass_class| subclass_class.is_instance_of(db, target_class)), - // Other than the cases enumerated above, `type[]` just delegates to `Instance("type")` - (Type::SubclassOf(_), _) => KnownClass::Type.to_instance(db).is_subtype_of(db, target), + // Other than the cases enumerated above, `type[]` and class-literal types just delegate to `Instance("type")` + (Type::SubclassOf(_) | Type::ClassLiteral(_), _) => { + KnownClass::Type.to_instance(db).is_subtype_of(db, target) + } // For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::Instance(_SpecialForm)`, // because `Type::KnownInstance(KnownInstanceType::Type)` is a set with exactly one runtime value in it @@ -922,19 +934,6 @@ impl<'db> Type<'db> { left.instance_fallback(db).is_subtype_of(db, right) } - // For example, `abc.ABCMeta` (== `Instance("abc.ABCMeta")`) is a subtype of `type[object]` - // (== `SubclassOf("object")`) because (since `abc.ABCMeta` subclasses `type`) all instances of `ABCMeta` - // are instances of `type`, and `type[object]` represents the set of all subclasses of `object`, - // which is exactly equal to the set of all instances of `type`. - ( - Type::Instance(_), - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(target_class), - }), - ) if target_class.is_known(db, KnownClass::Object) => { - self.is_subtype_of(db, KnownClass::Type.to_instance(db)) - } - // `bool` is a subtype of `int`, because `bool` subclasses `int`, // which means that all instances of `bool` are also instances of `int` (Type::Instance(self_instance), Type::Instance(target_instance)) => { @@ -975,30 +974,28 @@ impl<'db> Type<'db> { }, ) } - ( - Type::SubclassOf(SubclassOfType { - base: ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown, - }), - Type::SubclassOf(_), - ) => true, - ( - Type::SubclassOf(SubclassOfType { - base: ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown, - }), - Type::Instance(_), - ) if target.is_assignable_to(db, KnownClass::Type.to_instance(db)) => true, - ( - Type::Instance(_), - Type::SubclassOf(SubclassOfType { - base: ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown, - }), - ) if self.is_assignable_to(db, KnownClass::Type.to_instance(db)) => true, - ( - Type::ClassLiteral(_) | Type::SubclassOf(_), - Type::SubclassOf(SubclassOfType { - base: ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown, - }), - ) => true, + (Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_)) + if subclass_of_ty.is_dynamic() => + { + true + } + (Type::SubclassOf(subclass_of_ty), Type::Instance(_)) + if subclass_of_ty.is_dynamic() + && target.is_assignable_to(db, KnownClass::Type.to_instance(db)) => + { + true + } + (Type::Instance(_), Type::SubclassOf(subclass_of_ty)) + if subclass_of_ty.is_dynamic() + && self.is_assignable_to(db, KnownClass::Type.to_instance(db)) => + { + true + } + (Type::ClassLiteral(_) | Type::SubclassOf(_), Type::SubclassOf(target_subclass_of)) + if target_subclass_of.is_dynamic() => + { + true + } // TODO other types containing gradual forms (e.g. generics containing Any/Unknown) _ => self.is_subtype_of(db, target), } @@ -1014,33 +1011,9 @@ impl<'db> Type<'db> { return false; } - // type[object] ≡ type - if let ( - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(object_class), - }), - Type::Instance(InstanceType { class: type_class }), - ) - | ( - Type::Instance(InstanceType { class: type_class }), - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(object_class), - }), - ) = (self, other) - { - // This is the only case where "instance of a class" is equivalent to "subclass of a - // class", so we don't need to fall through if we're not looking at instance[type] and - // type[object] specifically. - return object_class.is_known(db, KnownClass::Object) - && type_class.is_known(db, KnownClass::Type); - } - // TODO equivalent but not identical structural types, differently-ordered unions and // intersections, other cases? - // TODO: Once we have support for final classes, we can establish that - // `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`. - // For all other cases, types are equivalent iff they have the same internal // representation. self == other @@ -1140,17 +1113,16 @@ impl<'db> Type<'db> { ) => true, ( - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(class_a), - }), + Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(ClassLiteralType { class: class_b }), ) | ( Type::ClassLiteral(ClassLiteralType { class: class_b }), - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(class_a), - }), - ) => !class_b.is_subclass_of(db, class_a), + Type::SubclassOf(subclass_of_ty), + ) => match subclass_of_ty.subclass_of() { + ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown => false, + ClassBase::Class(class_a) => !class_b.is_subclass_of(db, class_a), + }, (Type::SubclassOf(_), Type::SubclassOf(_)) => false, @@ -1198,10 +1170,10 @@ impl<'db> Type<'db> { } (Type::SubclassOf(_), other) | (other, Type::SubclassOf(_)) => { - // TODO: Once we have support for final classes, we can determine disjointness in more cases - // here. However, note that it might be better to turn `Type::SubclassOf('FinalClass')` into - // `Type::ClassLiteral('FinalClass')` during construction, instead of adding special cases for - // final classes inside `Type::SubclassOf` everywhere. + // TODO we could do better here: if both variants are `SubclassOf` and they have different "solid bases", + // multiple inheritance between the two is impossible, so they are disjoint. + // + // Note that `type[<@final class>]` is eagerly simplified to `Literal[<@final class>]` by [`SubclassOfType::from`]. other.is_disjoint_from(db, KnownClass::Type.to_instance(db)) } @@ -1357,7 +1329,7 @@ impl<'db> Type<'db> { | Type::KnownInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => true, - Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)), + 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 // bases are fully static, and only return `true` if that is the case. @@ -1421,11 +1393,8 @@ impl<'db> Type<'db> { // are both of type Literal[345], for example. false } - Type::SubclassOf(..) => { - // TODO once we have support for final classes, we can return `true` for some - // cases: type[C] is a singleton if C is final. - false - } + // We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton. + Type::SubclassOf(..) => false, Type::BooleanLiteral(_) | Type::FunctionLiteral(..) | Type::ClassLiteral(..) @@ -1678,7 +1647,8 @@ impl<'db> Type<'db> { Type::ClassLiteral(ClassLiteralType { class }) => { class.metaclass(db).to_instance(db).bool(db) } - Type::SubclassOf(SubclassOfType { base }) => base + Type::SubclassOf(subclass_of_ty) => subclass_of_ty + .subclass_of() .into_class() .map(|class| Type::class_literal(class).bool(db)) .unwrap_or(Truthiness::Ambiguous), @@ -1972,11 +1942,11 @@ impl<'db> Type<'db> { Type::Unknown => Type::Unknown, Type::Never => Type::Never, Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class), - Type::SubclassOf(SubclassOfType { base }) => match base { - ClassBase::Class(class) => Type::instance(*class), + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + ClassBase::Class(class) => Type::instance(class), ClassBase::Any => Type::Any, ClassBase::Unknown => Type::Unknown, - ClassBase::Todo(todo) => Type::Todo(*todo), + ClassBase::Todo(todo) => Type::Todo(todo), }, Type::Union(union) => union.map(db, |element| element.to_instance(db)), Type::Intersection(_) => todo_type!("Type::Intersection.to_instance()"), @@ -2131,7 +2101,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 }) => Type::subclass_of(*class), + Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class), Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db), @@ -2142,23 +2112,25 @@ impl<'db> Type<'db> { Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db), - Type::SubclassOf(SubclassOfType { base }) => match base { + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_) => *self, - ClassBase::Class(class) => Type::subclass_of_base( + ClassBase::Class(class) => SubclassOfType::from( + db, ClassBase::try_from_ty(db, class.metaclass(db)).unwrap_or(ClassBase::Unknown), ), }, Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), - Type::Any => Type::subclass_of_base(ClassBase::Any), - Type::Unknown => Type::subclass_of_base(ClassBase::Unknown), + Type::Any => SubclassOfType::subclass_of_any(), + Type::Unknown => SubclassOfType::subclass_of_unknown(), // TODO intersections - Type::Intersection(_) => Type::subclass_of_base( + Type::Intersection(_) => SubclassOfType::from( + db, ClassBase::try_from_ty(db, todo_type!("Intersection meta-type")) .expect("Type::Todo should be a valid ClassBase"), ), Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), - Type::Todo(todo) => Type::subclass_of_base(ClassBase::Todo(*todo)), + Type::Todo(todo) => SubclassOfType::from(db, ClassBase::Todo(*todo)), } } @@ -2367,8 +2339,8 @@ impl<'db> KnownClass { pub fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) .into_class_literal() - .map(|ClassLiteralType { class }| Type::subclass_of(class)) - .unwrap_or(Type::subclass_of_base(ClassBase::Unknown)) + .map(|ClassLiteralType { 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, @@ -2766,6 +2738,7 @@ impl<'db> KnownInstanceType<'db> { self.class().to_instance(db) } + /// Return `true` if this symbol is an instance of `class`. pub fn is_instance_of(self, db: &'db dyn Db, class: Class<'db>) -> bool { self.class().is_subclass_of(db, class) } @@ -3359,7 +3332,7 @@ impl<'db> Class<'db> { /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { self.try_metaclass(db) - .unwrap_or_else(|_| Type::subclass_of_base(ClassBase::Unknown)) + .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) } /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. @@ -3372,7 +3345,7 @@ impl<'db> Class<'db> { // We emit diagnostics for cyclic class definitions elsewhere. // Avoid attempting to infer the metaclass if the class is cyclically defined: // it would be easy to enter an infinite loop. - return Ok(Type::subclass_of_base(ClassBase::Unknown)); + return Ok(SubclassOfType::subclass_of_unknown()); } let explicit_metaclass = self.explicit_metaclass(db); @@ -3549,34 +3522,6 @@ 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, salsa::Update)] -pub struct SubclassOfType<'db> { - base: ClassBase<'db>, -} - -impl<'db> SubclassOfType<'db> { - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { - Type::from(self.base).member(db, name) - } - - fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool { - match (self.base, other.base) { - // Non-fully-static types do not participate in subtyping - (ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_), _) - | (_, ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_)) => false, - - // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, - // and `type[int]` describes all possible runtime subclasses of the class `int`. - // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. - (ClassBase::Class(self_class), ClassBase::Class(other_class)) => { - // N.B. The subclass relation is fully static - self_class.is_subclass_of(db, other_class) - } - } - } -} - /// 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> { @@ -3849,15 +3794,17 @@ pub(crate) mod tests { let elements = tys.into_iter().map(|ty| ty.into_type(db)); TupleType::from_elements(db, elements) } - Ty::SubclassOfAny => Type::subclass_of_base(ClassBase::Any), - Ty::SubclassOfUnknown => Type::subclass_of_base(ClassBase::Unknown), - Ty::SubclassOfBuiltinClass(s) => Type::subclass_of( + Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), + Ty::SubclassOfUnknown => SubclassOfType::subclass_of_unknown(), + Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( + db, builtins_symbol(db, s) .expect_type() .expect_class_literal() .class, ), - Ty::SubclassOfAbcClass(s) => Type::subclass_of( + Ty::SubclassOfAbcClass(s) => SubclassOfType::from( + db, known_module_symbol(db, KnownModule::Abc, s) .expect_type() .expect_class_literal() @@ -4019,6 +3966,7 @@ pub(crate) mod tests { #[test_case(Ty::AlwaysFalsy, Ty::BuiltinInstance("object"))] #[test_case(Ty::Never, Ty::AlwaysTruthy)] #[test_case(Ty::Never, Ty::AlwaysFalsy)] + #[test_case(Ty::BuiltinClassLiteral("bool"), Ty::SubclassOfBuiltinClass("int"))] 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))); @@ -4089,11 +4037,12 @@ pub(crate) mod tests { assert!(literal_derived.is_class_literal()); // `subclass_of_base` represents `Type[Base]`. - let subclass_of_base = Type::subclass_of(literal_base.expect_class_literal().class); + let subclass_of_base = SubclassOfType::from(&db, literal_base.expect_class_literal().class); assert!(literal_base.is_subtype_of(&db, subclass_of_base)); assert!(literal_derived.is_subtype_of(&db, subclass_of_base)); - let subclass_of_derived = Type::subclass_of(literal_derived.expect_class_literal().class); + let subclass_of_derived = + SubclassOfType::from(&db, literal_derived.expect_class_literal().class); assert!(literal_derived.is_subtype_of(&db, subclass_of_derived)); assert!(!literal_base.is_subtype_of(&db, subclass_of_derived)); @@ -4274,8 +4223,8 @@ pub(crate) mod tests { let literal_a = super::global_symbol(&db, module, "A").expect_type(); let literal_b = super::global_symbol(&db, module, "B").expect_type(); - let subclass_of_a = Type::subclass_of(literal_a.expect_class_literal().class); - let subclass_of_b = Type::subclass_of(literal_b.expect_class_literal().class); + let subclass_of_a = SubclassOfType::from(&db, literal_a.expect_class_literal().class); + let subclass_of_b = SubclassOfType::from(&db, literal_b.expect_class_literal().class); // Class literals are always disjoint. They are singleton types assert!(literal_a.is_disjoint_from(&db, literal_b)); @@ -4353,6 +4302,7 @@ pub(crate) mod tests { #[test_case(Ty::None)] #[test_case(Ty::BooleanLiteral(true))] #[test_case(Ty::BooleanLiteral(false))] + #[test_case(Ty::SubclassOfBuiltinClass("bool"))] // a `@final` class fn is_singleton(from: Ty) { let db = setup_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 4bd2fea206658..f08e4de444ad1 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -18,6 +18,13 @@ pub enum ClassBase<'db> { } impl<'db> ClassBase<'db> { + pub const fn is_dynamic(self) -> bool { + match self { + ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_) => true, + ClassBase::Class(_) => false, + } + } + pub 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/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index fa8014d91bacf..ded048b07c63c 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape; use crate::types::class_base::ClassBase; use crate::types::{ - ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, - SubclassOfType, Type, UnionType, + ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type, + UnionType, }; use crate::Db; use rustc_hash::FxHashMap; @@ -84,16 +84,14 @@ impl Display for DisplayRepresentation<'_> { } // TODO functions and classes should display using a fully qualified name Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)), - Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(class), - }) => { + 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]. - write!(f, "type[{}]", class.name(self.db)) - } - Type::SubclassOf(SubclassOfType { base }) => { - write!(f, "type[{}]", base.display(self.db)) - } + ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)), + base @ (ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown) => { + write!(f, "type[{}]", base.display(self.db)) + } + }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => f.write_str(function.name(self.db)), Type::Union(union) => union.display(self.db).fmt(f), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9a906ff1365ef..04e19a6e63d94 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -49,7 +49,6 @@ use crate::semantic_index::semantic_index; use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId}; use crate::semantic_index::SemanticIndex; use crate::stdlib::builtins_module_scope; -use crate::types::class_base::ClassBase; use crate::types::diagnostic::{ report_invalid_assignment, report_unresolved_module, TypeCheckDiagnostics, CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, @@ -65,7 +64,7 @@ use crate::types::{ typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, - Symbol, Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, + SubclassOfType, Symbol, Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::Unpack; @@ -4903,9 +4902,11 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Name(_) | ast::Expr::Attribute(_) => { let name_ty = self.infer_expression(slice); match name_ty { - Type::ClassLiteral(ClassLiteralType { class }) => Type::subclass_of(class), + Type::ClassLiteral(ClassLiteralType { class }) => { + SubclassOfType::from(self.db(), class) + } Type::KnownInstance(KnownInstanceType::Any) => { - Type::subclass_of_base(ClassBase::Any) + SubclassOfType::subclass_of_any() } _ => todo_type!("unsupported type[X] special form"), } diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index cc572eeb7c124..7db4f4e5b37a9 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -7,8 +7,8 @@ use crate::semantic_index::expression::Expression; use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; use crate::semantic_index::symbol_table; use crate::types::{ - infer_expression_types, ClassBase, ClassLiteralType, IntersectionBuilder, KnownClass, - KnownFunction, SubclassOfType, Truthiness, Type, UnionBuilder, + infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, KnownFunction, + SubclassOfType, Truthiness, Type, UnionBuilder, }; use crate::Db; use itertools::Itertools; @@ -97,6 +97,11 @@ impl KnownConstraintFunction { /// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604 /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { + let constraint_fn = |class| match self { + KnownConstraintFunction::IsInstance => Type::instance(class), + KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class), + }; + match classinfo { Type::Tuple(tuple) => { let mut builder = UnionBuilder::new(db); @@ -105,13 +110,10 @@ impl KnownConstraintFunction { } Some(builder.build()) } - Type::ClassLiteral(ClassLiteralType { class }) - | Type::SubclassOf(SubclassOfType { - base: ClassBase::Class(class), - }) => Some(match self { - KnownConstraintFunction::IsInstance => Type::instance(class), - KnownConstraintFunction::IsSubclass => Type::subclass_of(class), - }), + Type::ClassLiteral(ClassLiteralType { class }) => Some(constraint_fn(class)), + Type::SubclassOf(subclass_of_ty) => { + subclass_of_ty.subclass_of().into_class().map(constraint_fn) + } _ => 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 new file mode 100644 index 0000000000000..75d82e2c3419e --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -0,0 +1,92 @@ +use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, 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)] +pub struct SubclassOfType<'db> { + // Keep this field private, so that the only way of constructing the struct is through the `from` method. + subclass_of: ClassBase<'db>, +} + +impl<'db> SubclassOfType<'db> { + /// Construct a new [`Type`] instance representing a given class object (or a given dynamic type) + /// and all possible subclasses of that class object/dynamic type. + /// + /// This method does not always return a [`Type::SubclassOf`] variant. + /// If the class object is known to be a final class, + /// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type. + /// If the class object is `builtins.object`, `Type::Instance()` will be returned; + /// this is no more precise, but it is exactly equivalent to `type[object]`. + /// + /// The eager normalization here means that we do not need to worry elsewhere about distinguishing + /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. + pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { + let subclass_of = subclass_of.into(); + match subclass_of { + ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_) => { + Type::SubclassOf(Self { subclass_of }) + } + ClassBase::Class(class) => { + if class.is_final(db) { + Type::ClassLiteral(ClassLiteralType { class }) + } else if class.is_known(db, KnownClass::Object) { + KnownClass::Type.to_instance(db) + } else { + Type::SubclassOf(Self { subclass_of }) + } + } + } + } + + /// Return a [`Type`] instance representing the type `type[Unknown]`. + pub(crate) const fn subclass_of_unknown() -> Type<'db> { + Type::SubclassOf(SubclassOfType { + subclass_of: ClassBase::Unknown, + }) + } + + /// Return a [`Type`] instance representing the type `type[Any]`. + pub(crate) const fn subclass_of_any() -> Type<'db> { + Type::SubclassOf(SubclassOfType { + subclass_of: ClassBase::Any, + }) + } + + /// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`. + pub(crate) const fn subclass_of(self) -> ClassBase<'db> { + self.subclass_of + } + + pub const fn is_dynamic(self) -> bool { + // Unpack `self` so that we're forced to update this method if any more fields are added in the future. + let Self { subclass_of } = self; + subclass_of.is_dynamic() + } + + pub const fn is_fully_static(self) -> bool { + !self.is_dynamic() + } + + pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + Type::from(self.subclass_of).member(db, name) + } + + /// Return `true` if `self` is a subtype of `other`. + /// + /// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant; + /// only fully static types participate in subtyping. + pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool { + match (self.subclass_of, other.subclass_of) { + // Non-fully-static types do not participate in subtyping + (ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_), _) + | (_, ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_)) => false, + + // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, + // and `type[int]` describes all possible runtime subclasses of the class `int`. + // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. + (ClassBase::Class(self_class), ClassBase::Class(other_class)) => { + // N.B. The subclass relation is fully static + self_class.is_subclass_of(db, other_class) + } + } + } +}