[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
This commit is contained in:
Shunsuke Shibayama
2025-12-23 09:16:03 +09:00
committed by GitHub
parent 664686bdbc
commit 06db474f20
2 changed files with 14 additions and 1 deletions

View File

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

View File

@@ -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])
}
}
}