Skip to content

Commit 2c6c3b5

Browse files
committed
Add a ProtocolInstanceType
1 parent e56f89f commit 2c6c3b5

File tree

2 files changed

+135
-48
lines changed

2 files changed

+135
-48
lines changed

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,7 +1779,7 @@ impl<'db> ProtocolClassLiteral<'db> {
17791779
/// It is illegal for a protocol class to have any instance attributes that are not declared
17801780
/// in the protocol's class body. If any are assigned to, they are not taken into account in
17811781
/// the protocol's list of members.
1782-
pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db ordermap::set::Slice<Name> {
1782+
pub(super) fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet<Name> {
17831783
/// The list of excluded members is subject to change between Python versions,
17841784
/// especially for dunders, but it probably doesn't matter *too* much if this
17851785
/// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa
@@ -1816,58 +1816,52 @@ impl<'db> ProtocolClassLiteral<'db> {
18161816
)
18171817
}
18181818

1819-
#[salsa::tracked(return_ref)]
1820-
fn cached_protocol_members<'db>(
1821-
db: &'db dyn Db,
1822-
class: ClassLiteralType<'db>,
1823-
) -> Box<ordermap::set::Slice<Name>> {
1824-
let mut members = FxOrderSet::default();
1819+
let mut members = FxOrderSet::default();
18251820

1826-
for parent_protocol in class
1827-
.iter_mro(db, None)
1828-
.filter_map(ClassBase::into_class)
1829-
.filter_map(|class| class.class_literal(db).0.into_protocol_class(db))
1830-
{
1831-
let parent_scope = parent_protocol.body_scope(db);
1832-
let use_def_map = use_def_map(db, parent_scope);
1833-
let symbol_table = symbol_table(db, parent_scope);
1834-
1835-
members.extend(
1836-
use_def_map
1837-
.all_public_declarations()
1838-
.flat_map(|(symbol_id, declarations)| {
1839-
symbol_from_declarations(db, declarations)
1840-
.map(|symbol| (symbol_id, symbol))
1841-
})
1842-
.filter_map(|(symbol_id, symbol)| {
1843-
symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id)
1844-
})
1845-
// Bindings in the class body that are not declared in the class body
1846-
// are not valid protocol members, and we plan to emit diagnostics for them
1847-
// elsewhere. Invalid or not, however, it's important that we still consider
1848-
// them to be protocol members. The implementation of `issubclass()` and
1849-
// `isinstance()` for runtime-checkable protocols considers them to be protocol
1850-
// members at runtime, and it's important that we accurately understand
1851-
// type narrowing that uses `isinstance()` or `issubclass()` with
1852-
// runtime-checkable protocols.
1853-
.chain(use_def_map.all_public_bindings().filter_map(
1854-
|(symbol_id, bindings)| {
1821+
for parent_protocol in self
1822+
.iter_mro(db, None)
1823+
.filter_map(ClassBase::into_class)
1824+
.filter_map(|class| class.class_literal(db).0.into_protocol_class(db))
1825+
{
1826+
let parent_scope = parent_protocol.body_scope(db);
1827+
let use_def_map = use_def_map(db, parent_scope);
1828+
let symbol_table = symbol_table(db, parent_scope);
1829+
1830+
members.extend(
1831+
use_def_map
1832+
.all_public_declarations()
1833+
.flat_map(|(symbol_id, declarations)| {
1834+
symbol_from_declarations(db, declarations).map(|symbol| (symbol_id, symbol))
1835+
})
1836+
.filter_map(|(symbol_id, symbol)| {
1837+
symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id)
1838+
})
1839+
// Bindings in the class body that are not declared in the class body
1840+
// are not valid protocol members, and we plan to emit diagnostics for them
1841+
// elsewhere. Invalid or not, however, it's important that we still consider
1842+
// them to be protocol members. The implementation of `issubclass()` and
1843+
// `isinstance()` for runtime-checkable protocols considers them to be protocol
1844+
// members at runtime, and it's important that we accurately understand
1845+
// type narrowing that uses `isinstance()` or `issubclass()` with
1846+
// runtime-checkable protocols.
1847+
.chain(
1848+
use_def_map
1849+
.all_public_bindings()
1850+
.filter_map(|(symbol_id, bindings)| {
18551851
symbol_from_bindings(db, bindings)
18561852
.ignore_possibly_unbound()
18571853
.map(|_| symbol_id)
1858-
},
1859-
))
1860-
.map(|symbol_id| symbol_table.symbol(symbol_id).name())
1861-
.filter(|name| !excluded_from_proto_members(name))
1862-
.cloned(),
1863-
);
1864-
}
1865-
1866-
members.sort();
1867-
members.into_boxed_slice()
1854+
}),
1855+
)
1856+
.map(|symbol_id| symbol_table.symbol(symbol_id).name())
1857+
.filter(|name| !excluded_from_proto_members(name))
1858+
.cloned(),
1859+
);
18681860
}
18691861

1870-
cached_protocol_members(db, *self)
1862+
members.sort();
1863+
members.shrink_to_fit();
1864+
members
18711865
}
18721866

18731867
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {

crates/red_knot_python_semantic/src/types/instance.rs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Instance types: both nominal and structural.
22
3+
use ruff_python_ast::name::Name;
4+
35
use super::{ClassType, KnownClass, SubclassOfType, Type};
4-
use crate::Db;
6+
use crate::{Db, FxOrderSet};
57

68
impl<'db> Type<'db> {
79
pub(crate) const fn instance(class: ClassType<'db>) -> Self {
@@ -92,3 +94,94 @@ impl<'db> From<NominalInstanceType<'db>> for Type<'db> {
9294
Self::NominalInstance(value)
9395
}
9496
}
97+
98+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype)]
99+
pub enum ProtocolInstanceType<'db> {
100+
FromClass(ClassType<'db>),
101+
Synthesized(SynthesizedProtocolType<'db>),
102+
}
103+
104+
#[salsa::tracked]
105+
impl<'db> ProtocolInstanceType<'db> {
106+
#[salsa::tracked(return_ref)]
107+
fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet<Name> {
108+
match self {
109+
Self::FromClass(class) => class
110+
.class_literal(db)
111+
.0
112+
.into_protocol_class(db)
113+
.expect("Protocol class literal should be a protocol class")
114+
.protocol_members(db),
115+
Self::Synthesized(synthesized) => synthesized.members(db),
116+
}
117+
}
118+
119+
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
120+
match self {
121+
Self::FromClass(class) => SubclassOfType::from(db, class),
122+
123+
// TODO: we can and should do better here.
124+
//
125+
// This is supported by mypy, and should be supported by us as well.
126+
// We'll need to come up with a better solution for the meta-type of
127+
// synthesized protocols to solve this:
128+
//
129+
// ```py
130+
// from typing import Callable
131+
//
132+
// def foo(x: Callable[[], int]) -> None:
133+
// reveal_type(type(x)) # mypy: "type[def (builtins.int) -> builtins.str]"
134+
// reveal_type(type(x).__call__) # mypy: "def (*args: Any, **kwds: Any) -> Any"
135+
// ```
136+
Self::Synthesized(_) => KnownClass::Type.to_instance(db),
137+
}
138+
}
139+
140+
pub(super) fn normalized(self, db: &'db dyn Db) -> Self {
141+
match self {
142+
Self::FromClass(_) => {
143+
Self::Synthesized(SynthesizedProtocolType::new(db, self.protocol_members(db)))
144+
}
145+
Self::Synthesized(_) => self,
146+
}
147+
}
148+
149+
/// TODO: should not be considered fully static if any members do not have fully static types
150+
#[expect(clippy::unused_self)]
151+
pub(super) fn is_fully_static(self) -> bool {
152+
true
153+
}
154+
155+
/// TODO: consider the types of the members as well as their existence
156+
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
157+
self.protocol_members(db)
158+
.is_subset(other.protocol_members(db))
159+
}
160+
161+
/// TODO: consider the types of the members as well as their existence
162+
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
163+
self.is_subtype_of(db, other)
164+
}
165+
166+
/// TODO: consider the types of the members as well as their existence
167+
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
168+
self.protocol_members(db).set_eq(other.protocol_members(db))
169+
}
170+
171+
/// TODO: consider the types of the members as well as their existence
172+
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
173+
self.is_equivalent_to(db, other)
174+
}
175+
176+
/// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y`
177+
/// have a member with the same name but disjoint types
178+
#[expect(clippy::unused_self)]
179+
pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool {
180+
false
181+
}
182+
}
183+
184+
#[salsa::interned(debug)]
185+
pub struct SynthesizedProtocolType<'db> {
186+
members: FxOrderSet<Name>,
187+
}

0 commit comments

Comments
 (0)