Skip to content

Commit 2ae0bd9

Browse files
authored
[ty] Normalize recursive types using Any (#19003)
## Summary This just replaces one temporary solution to recursive protocols (the `SelfReference` mechanism) with another one (track seen types when recursively descending in `normalize` and replace recursive references with `Any`). But this temporary solution can handle mutually-recursive types, not just self-referential ones, and it's sufficient for the primer ecosystem and some other projects we are testing on to no longer stack overflow. The follow-up here will be to properly handle these self-references instead of replacing them with `Any`. We will also eventually need cycle detection on more recursive-descent type transformations and tests. ## Test Plan Existing tests (including recursive-protocol tests) and primer. Added mdtest for mutually-recursive protocols that stack-overflowed before this PR.
1 parent 34052a1 commit 2ae0bd9

File tree

14 files changed

+356
-368
lines changed

14 files changed

+356
-368
lines changed

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,21 @@ def _(r: Recursive):
17291729
reveal_type(r.method(r).callable1(1).direct.t[1][1]) # revealed: Recursive
17301730
```
17311731

1732+
### Mutually-recursive protocols
1733+
1734+
```py
1735+
from typing import Protocol
1736+
from ty_extensions import is_equivalent_to, static_assert
1737+
1738+
class Foo(Protocol):
1739+
x: "Bar"
1740+
1741+
class Bar(Protocol):
1742+
x: Foo
1743+
1744+
static_assert(is_equivalent_to(Foo, Bar))
1745+
```
1746+
17321747
### Regression test: narrowing with self-referential protocols
17331748

17341749
This snippet caused us to panic on an early version of the implementation for protocols.

crates/ty_python_semantic/resources/primer/bad.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jax # too many iterations
1010
mypy # too many iterations (self-recursive type alias)
1111
packaging # too many iterations
1212
pandas # slow (9s)
13-
pandera # stack overflow
13+
pandera # too many iterations
1414
pip # vendors packaging, see above
1515
pylint # cycle panics (self-recursive type alias)
1616
pyodide # too many cycle iterations
@@ -19,5 +19,4 @@ setuptools # vendors packaging, see above
1919
spack # slow, success, but mypy-primer hangs processing the output
2020
spark # too many iterations
2121
steam.py # hangs (single threaded)
22-
tornado # bad use-def map (https://github.com/astral-sh/ty/issues/365)
2322
xarray # too many iterations

crates/ty_python_semantic/resources/primer/good.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ strawberry
110110
streamlit
111111
svcs
112112
sympy
113+
tornado
113114
trio
114115
twine
115116
typeshed-stats

crates/ty_python_semantic/src/types.rs

Lines changed: 130 additions & 153 deletions
Large diffs are not rendered by default.

crates/ty_python_semantic/src/types/class.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use crate::types::tuple::TupleType;
2222
use crate::types::{
2323
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
2424
KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints,
25-
TypeVarInstance, TypeVarKind, infer_definition_types,
25+
TypeVarInstance, TypeVarKind, TypeVisitor, infer_definition_types,
2626
};
2727
use crate::{
2828
Db, FxOrderSet, KnownModule, Program,
@@ -182,8 +182,12 @@ pub struct GenericAlias<'db> {
182182
impl get_size2::GetSize for GenericAlias<'_> {}
183183

184184
impl<'db> GenericAlias<'db> {
185-
pub(super) fn normalized(self, db: &'db dyn Db) -> Self {
186-
Self::new(db, self.origin(db), self.specialization(db).normalized(db))
185+
pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
186+
Self::new(
187+
db,
188+
self.origin(db),
189+
self.specialization(db).normalized_impl(db, visitor),
190+
)
187191
}
188192

189193
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
@@ -249,10 +253,10 @@ pub enum ClassType<'db> {
249253

250254
#[salsa::tracked]
251255
impl<'db> ClassType<'db> {
252-
pub(super) fn normalized(self, db: &'db dyn Db) -> Self {
256+
pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
253257
match self {
254258
Self::NonGeneric(_) => self,
255-
Self::Generic(generic) => Self::Generic(generic.normalized(db)),
259+
Self::Generic(generic) => Self::Generic(generic.normalized_impl(db, visitor)),
256260
}
257261
}
258262

crates/ty_python_semantic/src/types/class_base.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::Db;
22
use crate::types::generics::Specialization;
33
use crate::types::{
44
ClassType, DynamicType, KnownClass, KnownInstanceType, MroError, MroIterator, SpecialFormType,
5-
Type, TypeMapping, todo_type,
5+
Type, TypeMapping, TypeVisitor, todo_type,
66
};
77

88
/// Enumeration of the possible kinds of types we allow in class bases.
@@ -31,10 +31,10 @@ impl<'db> ClassBase<'db> {
3131
Self::Dynamic(DynamicType::Unknown)
3232
}
3333

34-
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
34+
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
3535
match self {
3636
Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()),
37-
Self::Class(class) => Self::Class(class.normalized(db)),
37+
Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)),
3838
Self::Protocol | Self::Generic => self,
3939
}
4040
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use crate::FxOrderSet;
2+
use crate::types::Type;
3+
4+
#[derive(Debug, Default)]
5+
pub(crate) struct TypeVisitor<'db> {
6+
seen: FxOrderSet<Type<'db>>,
7+
}
8+
9+
impl<'db> TypeVisitor<'db> {
10+
pub(crate) fn visit(
11+
&mut self,
12+
ty: Type<'db>,
13+
func: impl FnOnce(&mut Self) -> Type<'db>,
14+
) -> Type<'db> {
15+
if !self.seen.insert(ty) {
16+
// TODO: proper recursive type handling
17+
18+
// This must be Any, not e.g. a todo type, because Any is the normalized form of the
19+
// dynamic type (that is, todo types are normalized to Any).
20+
return Type::any();
21+
}
22+
let ret = func(self);
23+
self.seen.pop();
24+
ret
25+
}
26+
}

crates/ty_python_semantic/src/types/function.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ use crate::types::narrow::ClassInfoConstraintFunction;
7575
use crate::types::signatures::{CallableSignature, Signature};
7676
use crate::types::{
7777
BoundMethodType, CallableType, DynamicType, KnownClass, Type, TypeMapping, TypeRelation,
78-
TypeVarInstance,
78+
TypeVarInstance, TypeVisitor,
7979
};
8080
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
8181

@@ -545,10 +545,10 @@ impl<'db> FunctionLiteral<'db> {
545545
}))
546546
}
547547

548-
fn normalized(self, db: &'db dyn Db) -> Self {
548+
fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
549549
let context = self
550550
.inherited_generic_context(db)
551-
.map(|ctx| ctx.normalized(db));
551+
.map(|ctx| ctx.normalized_impl(db, visitor));
552552
Self::new(db, self.last_definition(db), context)
553553
}
554554
}
@@ -819,12 +819,17 @@ impl<'db> FunctionType<'db> {
819819
}
820820

821821
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
822+
let mut visitor = TypeVisitor::default();
823+
self.normalized_impl(db, &mut visitor)
824+
}
825+
826+
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
822827
let mappings: Box<_> = self
823828
.type_mappings(db)
824829
.iter()
825-
.map(|mapping| mapping.normalized(db))
830+
.map(|mapping| mapping.normalized_impl(db, visitor))
826831
.collect();
827-
Self::new(db, self.literal(db).normalized(db), mappings)
832+
Self::new(db, self.literal(db).normalized_impl(db, visitor), mappings)
828833
}
829834
}
830835

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::types::signatures::{Parameter, Parameters, Signature};
1111
use crate::types::tuple::{TupleSpec, TupleType};
1212
use crate::types::{
1313
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
14-
TypeVarVariance, UnionType, declaration_type,
14+
TypeVarVariance, TypeVisitor, UnionType, declaration_type,
1515
};
1616
use crate::{Db, FxOrderSet};
1717

@@ -233,11 +233,11 @@ impl<'db> GenericContext<'db> {
233233
Specialization::new(db, self, expanded.into_boxed_slice(), None)
234234
}
235235

236-
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
236+
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
237237
let variables: FxOrderSet<_> = self
238238
.variables(db)
239239
.iter()
240-
.map(|ty| ty.normalized(db))
240+
.map(|ty| ty.normalized_impl(db, visitor))
241241
.collect();
242242
Self::new(db, variables)
243243
}
@@ -376,9 +376,15 @@ impl<'db> Specialization<'db> {
376376
Specialization::new(db, self.generic_context(db), types, None)
377377
}
378378

379-
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
380-
let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect();
381-
let tuple_inner = self.tuple_inner(db).and_then(|tuple| tuple.normalized(db));
379+
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
380+
let types: Box<[_]> = self
381+
.types(db)
382+
.iter()
383+
.map(|ty| ty.normalized_impl(db, visitor))
384+
.collect();
385+
let tuple_inner = self
386+
.tuple_inner(db)
387+
.and_then(|tuple| tuple.normalized_impl(db, visitor));
382388
Self::new(db, self.generic_context(db), types, tuple_inner)
383389
}
384390

@@ -526,9 +532,17 @@ impl<'db> PartialSpecialization<'_, 'db> {
526532
}
527533
}
528534

529-
pub(crate) fn normalized(&self, db: &'db dyn Db) -> PartialSpecialization<'db, 'db> {
530-
let generic_context = self.generic_context.normalized(db);
531-
let types: Cow<_> = self.types.iter().map(|ty| ty.normalized(db)).collect();
535+
pub(crate) fn normalized_impl(
536+
&self,
537+
db: &'db dyn Db,
538+
visitor: &mut TypeVisitor<'db>,
539+
) -> PartialSpecialization<'db, 'db> {
540+
let generic_context = self.generic_context.normalized_impl(db, visitor);
541+
let types: Cow<_> = self
542+
.types
543+
.iter()
544+
.map(|ty| ty.normalized_impl(db, visitor))
545+
.collect();
532546

533547
PartialSpecialization {
534548
generic_context,

crates/ty_python_semantic/src/types/instance.rs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::protocol_class::ProtocolInterface;
66
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
77
use crate::place::{Place, PlaceAndQualifiers};
88
use crate::types::tuple::TupleType;
9-
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
9+
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeVarInstance, TypeVisitor};
1010
use crate::{Db, FxOrderSet};
1111

1212
pub(super) use synthesized_protocol::SynthesizedProtocolType;
@@ -41,7 +41,11 @@ impl<'db> Type<'db> {
4141
M: IntoIterator<Item = (&'a str, Type<'db>)>,
4242
{
4343
Self::ProtocolInstance(ProtocolInstanceType::synthesized(
44-
SynthesizedProtocolType::new(db, ProtocolInterface::with_property_members(db, members)),
44+
SynthesizedProtocolType::new(
45+
db,
46+
ProtocolInterface::with_property_members(db, members),
47+
&mut TypeVisitor::default(),
48+
),
4549
))
4650
}
4751

@@ -80,8 +84,8 @@ impl<'db> NominalInstanceType<'db> {
8084
}
8185
}
8286

83-
pub(super) fn normalized(self, db: &'db dyn Db) -> Self {
84-
Self::from_class(self.class.normalized(db))
87+
pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
88+
Self::from_class(self.class.normalized_impl(db, visitor))
8589
}
8690

8791
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
@@ -201,31 +205,30 @@ impl<'db> ProtocolInstanceType<'db> {
201205
///
202206
/// See [`Type::normalized`] for more details.
203207
pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> {
208+
let mut visitor = TypeVisitor::default();
209+
self.normalized_impl(db, &mut visitor)
210+
}
211+
212+
/// Return a "normalized" version of this `Protocol` type.
213+
///
214+
/// See [`Type::normalized`] for more details.
215+
pub(super) fn normalized_impl(
216+
self,
217+
db: &'db dyn Db,
218+
visitor: &mut TypeVisitor<'db>,
219+
) -> Type<'db> {
204220
let object = KnownClass::Object.to_instance(db);
205221
if object.satisfies_protocol(db, self, TypeRelation::Subtyping) {
206222
return object;
207223
}
208224
match self.inner {
209225
Protocol::FromClass(_) => Type::ProtocolInstance(Self::synthesized(
210-
SynthesizedProtocolType::new(db, self.inner.interface(db)),
226+
SynthesizedProtocolType::new(db, self.inner.interface(db), visitor),
211227
)),
212228
Protocol::Synthesized(_) => Type::ProtocolInstance(self),
213229
}
214230
}
215231

216-
/// Replace references to `class` with a self-reference marker
217-
pub(super) fn replace_self_reference(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
218-
match self.inner {
219-
Protocol::FromClass(class_type) if class_type.class_literal(db).0 == class => {
220-
ProtocolInstanceType::synthesized(SynthesizedProtocolType::new(
221-
db,
222-
ProtocolInterface::SelfReference,
223-
))
224-
}
225-
_ => self,
226-
}
227-
}
228-
229232
/// Return `true` if the types of any of the members match the closure passed in.
230233
pub(super) fn any_over_type(
231234
self,
@@ -352,7 +355,7 @@ impl<'db> Protocol<'db> {
352355

353356
mod synthesized_protocol {
354357
use crate::types::protocol_class::ProtocolInterface;
355-
use crate::types::{TypeMapping, TypeVarInstance, TypeVarVariance};
358+
use crate::types::{TypeMapping, TypeVarInstance, TypeVarVariance, TypeVisitor};
356359
use crate::{Db, FxOrderSet};
357360

358361
/// A "synthesized" protocol type that is dissociated from a class definition in source code.
@@ -370,8 +373,12 @@ mod synthesized_protocol {
370373
pub(in crate::types) struct SynthesizedProtocolType<'db>(ProtocolInterface<'db>);
371374

372375
impl<'db> SynthesizedProtocolType<'db> {
373-
pub(super) fn new(db: &'db dyn Db, interface: ProtocolInterface<'db>) -> Self {
374-
Self(interface.normalized(db))
376+
pub(super) fn new(
377+
db: &'db dyn Db,
378+
interface: ProtocolInterface<'db>,
379+
visitor: &mut TypeVisitor<'db>,
380+
) -> Self {
381+
Self(interface.normalized_impl(db, visitor))
375382
}
376383

377384
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {

0 commit comments

Comments
 (0)