diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py b/crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py new file mode 100644 index 0000000000..28ba9d9091 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py @@ -0,0 +1,10 @@ +# Regression test for https://github.com/astral-sh/ruff/pull/20962 +# error message: +# `infer_definition_types(Id(1804)): execute: too many cycle iterations` + +for name_1 in { + {{0: name_4 for unique_name_0 in unique_name_1}: 0 for unique_name_2 in unique_name_3 if name_4}: 0 + for unique_name_4 in name_1 + for name_4 in name_1 +}: + pass diff --git a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md b/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md index b011d95e8c..97bbf21049 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md @@ -35,16 +35,3 @@ else: async def name_5(): pass ``` - -## Too many cycle iterations in `infer_definition_types` - - - -```py -for name_1 in { - {{0: name_4 for unique_name_0 in unique_name_1}: 0 for unique_name_2 in unique_name_3 if name_4}: 0 - for unique_name_4 in name_1 - for name_4 in name_1 -}: - pass -``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ffe0f3066b..e79282a35d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -873,6 +873,10 @@ impl<'db> Type<'db> { matches!(self, Type::Dynamic(_)) } + const fn is_non_divergent_dynamic(&self) -> bool { + self.is_dynamic() && !self.is_divergent() + } + /// Is a value of this type only usable in typing contexts? pub(crate) fn is_type_check_only(&self, db: &'db dyn Db) -> bool { match self { @@ -1695,22 +1699,33 @@ impl<'db> Type<'db> { // holds true if `T` is also a dynamic type or a union that contains a dynamic type. // Similarly, `T <: Any` only holds true if `T` is a dynamic type or an intersection // that contains a dynamic type. - (Type::Dynamic(_), _) => ConstraintSet::from(match relation { - TypeRelation::Subtyping => false, - TypeRelation::Assignability => true, - TypeRelation::Redundancy => match target { - Type::Dynamic(_) => true, - Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), - _ => false, - }, - }), + (Type::Dynamic(dynamic), _) => { + // If a `Divergent` type is involved, it must not be eliminated. + debug_assert!( + !matches!(dynamic, DynamicType::Divergent(_)), + "DynamicType::Divergent should have been handled in an earlier branch" + ); + ConstraintSet::from(match relation { + TypeRelation::Subtyping => false, + TypeRelation::Assignability => true, + TypeRelation::Redundancy => match target { + Type::Dynamic(_) => true, + Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), + _ => false, + }, + }) + } (_, Type::Dynamic(_)) => ConstraintSet::from(match relation { TypeRelation::Subtyping => false, TypeRelation::Assignability => true, TypeRelation::Redundancy => match self { Type::Dynamic(_) => true, Type::Intersection(intersection) => { - intersection.positive(db).iter().any(Type::is_dynamic) + // If a `Divergent` type is involved, it must not be eliminated. + intersection + .positive(db) + .iter() + .any(Type::is_non_divergent_dynamic) } _ => false, }, @@ -9991,6 +10006,10 @@ pub(crate) enum TypeRelation { /// materialization of `Any` and `int | Any` may be the same type (`object`), but the /// two differ in their bottom materializations (`Never` and `int`, respectively). /// + /// Despite the above principles, there is one exceptional type that should never be union-simplified: the `Divergent` type. + /// This is a kind of dynamic type, but it acts as a marker to track recursive type structures. + /// If this type is accidentally eliminated by simplification, the fixed-point iteration will not converge. + /// /// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type /// [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize Redundancy, @@ -12103,6 +12122,27 @@ pub(crate) mod tests { assert!(div.is_equivalent_to(&db, div)); assert!(!div.is_equivalent_to(&db, Type::unknown())); assert!(!Type::unknown().is_equivalent_to(&db, div)); + assert!(!div.is_redundant_with(&db, Type::unknown())); + assert!(!Type::unknown().is_redundant_with(&db, div)); + + let truthy_div = IntersectionBuilder::new(&db) + .add_positive(div) + .add_negative(Type::AlwaysFalsy) + .build(); + + let union = UnionType::from_elements(&db, [Type::unknown(), truthy_div]); + assert!(!truthy_div.is_redundant_with(&db, Type::unknown())); + assert_eq!( + union.display(&db).to_string(), + "Unknown | (Divergent & ~AlwaysFalsy)" + ); + + let union = UnionType::from_elements(&db, [truthy_div, Type::unknown()]); + assert!(!Type::unknown().is_redundant_with(&db, truthy_div)); + assert_eq!( + union.display(&db).to_string(), + "(Divergent & ~AlwaysFalsy) | Unknown" + ); // The `object` type has a good convergence property, that is, its union with all other types is `object`. // (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`)