[ty] Fix panic due to simplifying Divergent types out of intersections types (#21253)

This commit is contained in:
Shunsuke Shibayama 2025-11-04 00:41:11 +09:00 committed by GitHub
parent 39f105bc4a
commit b5305b5f32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 60 additions and 23 deletions

View file

@ -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

View file

@ -35,16 +35,3 @@ else:
async def name_5():
pass
```
## Too many cycle iterations in `infer_definition_types`
<!-- expect-panic: too many cycle iterations -->
```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
```

View file

@ -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`)