From 06db474f208b826e5801b08d67f208bec0ef030b Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:16:03 +0900 Subject: [PATCH] [ty] stabilize union-type ordering in fixed-point iteration (#22070) ## Summary This PR fixes https://github.com/astral-sh/ty/issues/2085. Based on the reported code, the panicking MRE is: ```python class Test: def __init__(self, x: int): self.left = x self.right = x def method(self): self.left, self.right = self.right, self.left if self.right: self.right = self.right ``` The type inference (`implicit_attribute_inner`) for `self.right` proceeds as follows: ``` 0: Divergent(Id(6c07)) 1: Unknown | int | (Divergent(Id(1c00)) & ~AlwaysFalsy) 2: Unknown | int | (Divergent(Id(6c07)) & ~AlwaysFalsy) | (Divergent(Id(1c00)) & ~AlwaysFalsy) 3: Unknown | int | (Divergent(Id(1c00)) & ~AlwaysFalsy) | (Divergent(Id(6c07)) & ~AlwaysFalsy) 4: Unknown | int | (Divergent(Id(6c07)) & ~AlwaysFalsy) | (Divergent(Id(1c00)) & ~AlwaysFalsy) ... ``` The problem is that the order of union types is not stable between cycles. To solve this, when unioning the previous union type with the current union type, we should use the previous type as the base and add only the new elements in this cycle (In the current implementation, this unioning order was reversed). ## Test Plan New corpus test --- .../resources/corpus/cyclic_implicit_attr_union.py | 10 ++++++++++ crates/ty_python_semantic/src/types.rs | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 crates/ty_python_semantic/resources/corpus/cyclic_implicit_attr_union.py diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_implicit_attr_union.py b/crates/ty_python_semantic/resources/corpus/cyclic_implicit_attr_union.py new file mode 100644 index 0000000000..ffce93c61f --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/cyclic_implicit_attr_union.py @@ -0,0 +1,10 @@ +# regression test for https://github.com/astral-sh/ty/issues/2085 + +class Foo: + def __init__(self, x: int): + self.left = x + self.right = x + def method(self): + self.left, self.right = self.right, self.left + if self.right: + self.right = self.right diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bcca8e77fe..cc05c9c000 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -965,7 +965,10 @@ impl<'db> Type<'db> { if has_divergent_type_in_cycle(previous) && !has_divergent_type_in_cycle(self) { self } else { - UnionType::from_elements_cycle_recovery(db, [self, previous]) + // The current type is unioned to the previous type. Unioning in the reverse order can cause the fixed-point iterations to converge slowly or even fail. + // Consider the case where the order of union types is different between the previous and current cycle. + // We should use the previous union type as the base and only add new element types in this cycle, if any. + UnionType::from_elements_cycle_recovery(db, [previous, self]) } } }