-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Fix the inferred interface of specialized generic protocols #19866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Diagnostic diff on typing conformance testsNo changes detected when running ty on typing conformance tests ✅ |
|
That is... many more ecosystem hits than I was expecting 😆 |
CodSpeed Instrumentation Performance ReportMerging #19866 will degrade performances by 13.81%Comparing Summary
Benchmarks breakdown
|
2f03837
to
81b99cc
Compare
If I apply this diff, it makes the diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs
index 7b82192ece..673aeecbf5 100644
--- a/crates/ty_python_semantic/src/types/protocol_class.rs
+++ b/crates/ty_python_semantic/src/types/protocol_class.rs
@@ -7,6 +7,7 @@ use ruff_python_ast::name::Name;
use super::TypeVarVariance;
use crate::semantic_index::place_table;
+use crate::types::generics::Specialization;
use crate::{
Db, FxOrderSet,
place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
@@ -124,6 +125,7 @@ impl<'db> ProtocolInterface<'db> {
ProtocolMemberData {
qualifiers: TypeQualifiers::default(),
kind: ProtocolMemberKind::Property(property),
+ specialization: None,
},
)
})
@@ -142,18 +144,34 @@ impl<'db> ProtocolInterface<'db> {
where
'db: 'a,
{
- self.inner(db).iter().map(|(name, data)| ProtocolMember {
- name,
- kind: data.kind,
- qualifiers: data.qualifiers,
+ self.inner(db).iter().map(|(name, data)| {
+ let ProtocolMemberData {
+ kind,
+ specialization,
+ qualifiers,
+ } = data;
+
+ ProtocolMember {
+ name,
+ kind: kind.apply_optional_specialization(db, *specialization),
+ qualifiers: *qualifiers,
+ }
})
}
fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option<ProtocolMember<'a, 'db>> {
- self.inner(db).get(name).map(|data| ProtocolMember {
- name,
- kind: data.kind,
- qualifiers: data.qualifiers,
+ self.inner(db).get(name).map(|data| {
+ let ProtocolMemberData {
+ kind,
+ specialization,
+ qualifiers,
+ } = data;
+
+ ProtocolMember {
+ name,
+ kind: kind.apply_optional_specialization(db, *specialization),
+ qualifiers: *qualifiers,
+ }
})
}
@@ -237,8 +255,13 @@ impl<'db> ProtocolInterface<'db> {
impl std::fmt::Display for ProtocolInterfaceDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char('{')?;
- for (i, (name, data)) in self.interface.inner(self.db).iter().enumerate() {
- write!(f, "\"{name}\": {data}", data = data.display(self.db))?;
+ for (i, member) in self.interface.members(self.db).enumerate() {
+ let ProtocolMember {
+ name,
+ kind,
+ qualifiers: _,
+ } = member;
+ write!(f, "\"{name}\": {kind}", kind = kind.display(self.db))?;
if i < self.interface.inner(self.db).len() - 1 {
f.write_str(", ")?;
}
@@ -257,6 +280,7 @@ impl<'db> ProtocolInterface<'db> {
#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)]
pub(super) struct ProtocolMemberData<'db> {
kind: ProtocolMemberKind<'db>,
+ specialization: Option<Specialization<'db>>,
qualifiers: TypeQualifiers,
}
@@ -268,6 +292,7 @@ impl<'db> ProtocolMemberData<'db> {
fn normalized_impl(&self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self {
Self {
kind: self.kind.normalized_impl(db, visitor),
+ specialization: self.specialization.map(|s| s.normalized_impl(db, visitor)),
qualifiers: self.qualifiers,
}
}
@@ -276,6 +301,9 @@ impl<'db> ProtocolMemberData<'db> {
Self {
kind: self.kind.apply_type_mapping(db, type_mapping),
qualifiers: self.qualifiers,
+ specialization: self
+ .specialization
+ .map(|s| s.apply_type_mapping(db, type_mapping)),
}
}
@@ -291,41 +319,7 @@ impl<'db> ProtocolMemberData<'db> {
Self {
kind: self.kind.materialize(db, variance),
qualifiers: self.qualifiers,
- }
- }
-
- fn display(&self, db: &'db dyn Db) -> impl std::fmt::Display {
- struct ProtocolMemberDataDisplay<'db> {
- db: &'db dyn Db,
- data: ProtocolMemberKind<'db>,
- }
-
- impl std::fmt::Display for ProtocolMemberDataDisplay<'_> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self.data {
- ProtocolMemberKind::Method(callable) => {
- write!(f, "MethodMember(`{}`)", callable.display(self.db))
- }
- ProtocolMemberKind::Property(property) => {
- let mut d = f.debug_struct("PropertyMember");
- if let Some(getter) = property.getter(self.db) {
- d.field("getter", &format_args!("`{}`", &getter.display(self.db)));
- }
- if let Some(setter) = property.setter(self.db) {
- d.field("setter", &format_args!("`{}`", &setter.display(self.db)));
- }
- d.finish()
- }
- ProtocolMemberKind::Other(ty) => {
- write!(f, "AttributeMember(`{}`)", ty.display(self.db))
- }
- }
- }
- }
-
- ProtocolMemberDataDisplay {
- db,
- data: self.kind,
+ specialization: self.specialization.map(|s| s.materialize(db, variance)),
}
}
}
@@ -366,6 +360,16 @@ impl<'db> ProtocolMemberKind<'db> {
}
}
+ fn apply_optional_specialization(
+ self,
+ db: &'db dyn Db,
+ specialization: Option<Specialization<'db>>,
+ ) -> Self {
+ specialization
+ .map(|s| self.apply_type_mapping(db, &TypeMapping::Specialization(s)))
+ .unwrap_or(self)
+ }
+
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
@@ -391,6 +395,38 @@ impl<'db> ProtocolMemberKind<'db> {
}
}
}
+
+ fn display(self, db: &'db dyn Db) -> impl std::fmt::Display {
+ struct ProtocolMemberKindDisplay<'db> {
+ db: &'db dyn Db,
+ kind: ProtocolMemberKind<'db>,
+ }
+
+ impl std::fmt::Display for ProtocolMemberKindDisplay<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self.kind {
+ ProtocolMemberKind::Method(callable) => {
+ write!(f, "MethodMember(`{}`)", callable.display(self.db))
+ }
+ ProtocolMemberKind::Property(property) => {
+ let mut d = f.debug_struct("PropertyMember");
+ if let Some(getter) = property.getter(self.db) {
+ d.field("getter", &format_args!("`{}`", &getter.display(self.db)));
+ }
+ if let Some(setter) = property.setter(self.db) {
+ d.field("setter", &format_args!("`{}`", &setter.display(self.db)));
+ }
+ d.finish()
+ }
+ ProtocolMemberKind::Other(ty) => {
+ write!(f, "AttributeMember(`{}`)", ty.display(self.db))
+ }
+ }
+ }
+ }
+
+ ProtocolMemberKindDisplay { db, kind: self }
+ }
}
/// A single member of a protocol interface.
@@ -581,8 +617,6 @@ fn cached_protocol_interface<'db>(
})
.filter(|(name, _, _, _)| !excluded_from_proto_members(name))
.map(|(name, ty, qualifiers, bound_on_class)| {
- let ty = ty.apply_optional_specialization(db, specialization);
-
let kind = match (ty, bound_on_class) {
// TODO: if the getter or setter is a function literal, we should
// upcast it to a `CallableType` so that two protocols with identical property
@@ -601,7 +635,12 @@ fn cached_protocol_interface<'db>(
_ => ProtocolMemberKind::Other(ty),
};
- let member = ProtocolMemberData { kind, qualifiers };
+ let member = ProtocolMemberData {
+ kind,
+ specialization,
+ qualifiers,
+ };
+
(name.clone(), member)
}),
); I guess I should try to minimize that stack overflow and add an mdtest... |
minimized: from typing import Protocol
class Foo[T](Protocol):
def x(self) -> "T | Foo[T]": ...
x: str | Foo[str] |
To debug a stack overflow, the first thing you need is a backtrace. To get a backtrace, run the test binary directly (instead of via |
81b99cc
to
8f63344
Compare
2afdca5
to
2d7979f
Compare
This is the section of the backtrace that repeats unendingly if I apply the patch in #19866 (comment):
It's not totally obvious to me how to fix this with our existing |
14cc006
to
3a5fe73
Compare
What this looks like is that in the process of specializing a protocol type, we have to map over a union in I agree this isn't easily amenable to our existing tools. Like the tuple-subtype-check-builds-a-union-of-its-elements issue I initially ran into in the recursive-type-aliases PR, the problem is the cycle passes through UnionBuilder and The part about "subtype check of a protocol requires applying a type mapping to it" feels a bit off to me (just in that applying a type mapping is kind of a heavyweight transformation that builds a new type, and it doesn't seem like we should have to do that just in order to make a subtype check), but I'm not sure if it's wrong or not without digging a lot deeper into this diff. Assuming that part is necessary, another way to break the cycle could be to re-introduce Or to find another place we can insert a Salsa query to catch and iterate on the cycle (like we did for In general, we have decent tools now for preventing cycles within a Honestly, a very comprehensive fix for this would be to make e.g. |
3a5fe73
to
4e2f8e9
Compare
|
Lint rule | Added | Removed | Changed |
---|---|---|---|
possibly-unbound-attribute |
0 | 43 | 2 |
invalid-return-type |
0 | 17 | 0 |
unresolved-attribute |
0 | 17 | 0 |
invalid-argument-type |
0 | 12 | 2 |
redundant-cast |
0 | 14 | 0 |
unsupported-operator |
0 | 6 | 0 |
non-subscriptable |
0 | 5 | 0 |
no-matching-overload |
0 | 4 | 0 |
index-out-of-bounds |
0 | 3 | 0 |
invalid-assignment |
0 | 3 | 0 |
unused-ignore-comment |
3 | 0 | 0 |
Total | 3 | 124 | 4 |
Ecosystem analysisI haven't gone through every single hit but I've gone through enough to be fairly confident that everything here is basically working as expected. Most of the hits are due to interesting interactions between protocol assignability/equivalence and the overload evaluation algorithm.
|
4e2f8e9
to
2648834
Compare
2648834
to
4f1d7e1
Compare
Okay, I think this is now ready for review! I've updated the PR description. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
* main: [`ruff`] Preserve relative whitespace in multi-line expressions (`RUF033`) (astral-sh#19647) [ty] Optimize TDD atom ordering (astral-sh#20098) [`airflow`] Extend `AIR311` and `AIR312` rules (astral-sh#20082) [ty] Preserve qualifiers when accessing attributes on unions/intersections (astral-sh#20114) [ty] Fix the inferred interface of specialized generic protocols (astral-sh#19866) [ty] Infer slightly more precise types for comprehensions (astral-sh#20111) [ty] Add more tests for protocols (astral-sh#20095) [ty] don't eagerly unpack aliases in user-authored unions (astral-sh#20055) [`flake8-use-pathlib`] Update links to the table showing the correspondence between `os` and `pathlib` (astral-sh#20103) [`flake8-use-pathlib`] Make `PTH100` fix unsafe because it can change behavior (astral-sh#20100) [`flake8-use-pathlib`] Delete unused `Rule::OsSymlink` enabled check (astral-sh#20099) [ty] Add search paths info to unresolved import diagnostics (astral-sh#20040) [`flake8-logging-format`] Add auto-fix for f-string logging calls (`G004`) (astral-sh#19303) Add a `ScopeKind` for the `__class__` cell (astral-sh#20048) Fix incorrect D413 links in docstrings convention FAQ (astral-sh#20089) [ty] Refactor inlay hints structure to use separate parts (astral-sh#20052)
Summary
This PR fixes a number of issues with our inference for generic protocols:
SupportsAbs[int]
should internally be considered a protocol class, even thoughis_protocol(SupportsAbs[int])
returnsFalse
at runtimeSupportsAbs
should be{"__abs__": MethodMember(`(self) -> Unknown`)}
, not{"__abs__": MethodMember(`(self) -> _T_co@SupportsAbs`)}
SupportsAbs[int]
should be{"__abs__": MethodMember(`(self) -> int`)}
This PR results in quite a few ecosystem diagnostics going away, which I didn't expect! I analysed them in #19866 (comment). Most of them are due to interactions between protocol assignability/equivalence and the overload call evaluation algorithm.
There's a fairly steep regression on the
DateType
benchmark here, but most of the execution time forDateType
is spent doing subtype checks between some fairly huge protocols; it's an extreme case. There aren't significant regressions on other benchmarks. I tried out an optimization where we only lazily specialized each protocol member, rather than eagerly specializing the whole interface incached_protocol_interface
(decb3d4). This did indeed speedup theDateType
benchmark quite a bit, but caused stack overflows onoptuna
which weren't fixable without adding new Salsa queries -- and adding the new Salsa queries undid the speedup onDateType
. See #19866 (comment), #19866 (comment) and #19866 (comment). I've therefore backed out that attempted optimisation; I think at this point we just have to eat the regression on this (somewhat strange and specific) benchmark, unless anybody else has any ideas! A regression test for theoptuna
stack overflow was added in #20095.Test Plan
Mdtests added and updated