From 629efb02f1afa930f9ecb65f35fbe2b8c0fbf3ed Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 24 Apr 2025 13:53:22 +0100 Subject: [PATCH] Add a `ProtocolInstanceType` --- .../src/types/class.rs | 86 ++++++++--------- .../src/types/instance.rs | 95 ++++++++++++++++++- 2 files changed, 134 insertions(+), 47 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index b3c28473fc..e158851bb9 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1711,7 +1711,7 @@ impl<'db> ProtocolClassLiteral<'db> { /// It is illegal for a protocol class to have any instance attributes that are not declared /// in the protocol's class body. If any are assigned to, they are not taken into account in /// the protocol's list of members. - pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db ordermap::set::Slice { + pub(super) fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet { /// The list of excluded members is subject to change between Python versions, /// especially for dunders, but it probably doesn't matter *too* much if this /// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa @@ -1748,58 +1748,52 @@ impl<'db> ProtocolClassLiteral<'db> { ) } - #[salsa::tracked(return_ref)] - fn cached_protocol_members<'db>( - db: &'db dyn Db, - class: ClassLiteral<'db>, - ) -> Box> { - let mut members = FxOrderSet::default(); + let mut members = FxOrderSet::default(); - for parent_protocol in class - .iter_mro(db, None) - .filter_map(ClassBase::into_class) - .filter_map(|class| class.class_literal(db).0.into_protocol_class(db)) - { - let parent_scope = parent_protocol.body_scope(db); - let use_def_map = use_def_map(db, parent_scope); - let symbol_table = symbol_table(db, parent_scope); + for parent_protocol in self + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .filter_map(|class| class.class_literal(db).0.into_protocol_class(db)) + { + let parent_scope = parent_protocol.body_scope(db); + let use_def_map = use_def_map(db, parent_scope); + let symbol_table = symbol_table(db, parent_scope); - members.extend( - use_def_map - .all_public_declarations() - .flat_map(|(symbol_id, declarations)| { - symbol_from_declarations(db, declarations) - .map(|symbol| (symbol_id, symbol)) - }) - .filter_map(|(symbol_id, symbol)| { - symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id) - }) - // Bindings in the class body that are not declared in the class body - // are not valid protocol members, and we plan to emit diagnostics for them - // elsewhere. Invalid or not, however, it's important that we still consider - // them to be protocol members. The implementation of `issubclass()` and - // `isinstance()` for runtime-checkable protocols considers them to be protocol - // members at runtime, and it's important that we accurately understand - // type narrowing that uses `isinstance()` or `issubclass()` with - // runtime-checkable protocols. - .chain(use_def_map.all_public_bindings().filter_map( - |(symbol_id, bindings)| { + members.extend( + use_def_map + .all_public_declarations() + .flat_map(|(symbol_id, declarations)| { + symbol_from_declarations(db, declarations).map(|symbol| (symbol_id, symbol)) + }) + .filter_map(|(symbol_id, symbol)| { + symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id) + }) + // Bindings in the class body that are not declared in the class body + // are not valid protocol members, and we plan to emit diagnostics for them + // elsewhere. Invalid or not, however, it's important that we still consider + // them to be protocol members. The implementation of `issubclass()` and + // `isinstance()` for runtime-checkable protocols considers them to be protocol + // members at runtime, and it's important that we accurately understand + // type narrowing that uses `isinstance()` or `issubclass()` with + // runtime-checkable protocols. + .chain( + use_def_map + .all_public_bindings() + .filter_map(|(symbol_id, bindings)| { symbol_from_bindings(db, bindings) .ignore_possibly_unbound() .map(|_| symbol_id) - }, - )) - .map(|symbol_id| symbol_table.symbol(symbol_id).name()) - .filter(|name| !excluded_from_proto_members(name)) - .cloned(), - ); - } - - members.sort(); - members.into_boxed_slice() + }), + ) + .map(|symbol_id| symbol_table.symbol(symbol_id).name()) + .filter(|name| !excluded_from_proto_members(name)) + .cloned(), + ); } - cached_protocol_members(db, *self) + members.sort(); + members.shrink_to_fit(); + members } pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs index e6e9785e16..da8eeb78ee 100644 --- a/crates/red_knot_python_semantic/src/types/instance.rs +++ b/crates/red_knot_python_semantic/src/types/instance.rs @@ -1,7 +1,9 @@ //! Instance types: both nominal and structural. +use ruff_python_ast::name::Name; + use super::{ClassType, KnownClass, SubclassOfType, Type}; -use crate::Db; +use crate::{Db, FxOrderSet}; impl<'db> Type<'db> { pub(crate) const fn instance(class: ClassType<'db>) -> Self { @@ -92,3 +94,94 @@ impl<'db> From> for Type<'db> { Self::NominalInstance(value) } } + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype)] +pub enum ProtocolInstanceType<'db> { + FromClass(ClassType<'db>), + Synthesized(SynthesizedProtocolType<'db>), +} + +#[salsa::tracked] +impl<'db> ProtocolInstanceType<'db> { + #[salsa::tracked(return_ref)] + fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet { + match self { + Self::FromClass(class) => class + .class_literal(db) + .0 + .into_protocol_class(db) + .expect("Protocol class literal should be a protocol class") + .protocol_members(db), + Self::Synthesized(synthesized) => synthesized.members(db), + } + } + + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + match self { + Self::FromClass(class) => SubclassOfType::from(db, class), + + // TODO: we can and should do better here. + // + // This is supported by mypy, and should be supported by us as well. + // We'll need to come up with a better solution for the meta-type of + // synthesized protocols to solve this: + // + // ```py + // from typing import Callable + // + // def foo(x: Callable[[], int]) -> None: + // reveal_type(type(x)) # mypy: "type[def (builtins.int) -> builtins.str]" + // reveal_type(type(x).__call__) # mypy: "def (*args: Any, **kwds: Any) -> Any" + // ``` + Self::Synthesized(_) => KnownClass::Type.to_instance(db), + } + } + + pub(super) fn normalized(self, db: &'db dyn Db) -> Self { + match self { + Self::FromClass(_) => { + Self::Synthesized(SynthesizedProtocolType::new(db, self.protocol_members(db))) + } + Self::Synthesized(_) => self, + } + } + + /// TODO: should not be considered fully static if any members do not have fully static types + #[expect(clippy::unused_self)] + pub(super) fn is_fully_static(self) -> bool { + true + } + + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + self.protocol_members(db) + .is_subset(other.protocol_members(db)) + } + + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { + self.is_subtype_of(db, other) + } + + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.protocol_members(db).set_eq(other.protocol_members(db)) + } + + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.is_equivalent_to(db, other) + } + + /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` + /// have a member with the same name but disjoint types + #[expect(clippy::unused_self)] + pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool { + false + } +} + +#[salsa::interned(debug)] +pub struct SynthesizedProtocolType<'db> { + members: FxOrderSet, +}