[ty] Avoid exponential invariant constraint paths (#26538)
## Summary
When a type variable is compared with a concrete type inside an
invariant generic, lazy constraint construction previously represented
equivalence as two independent relation constraints. Combining those
constraints across an outer union multiplied their alternative paths,
causing the issue's 21-arm generic-call example to exhibit
exponential-looking runtime.
This PR constructs a single bounded type-variable constraint directly
when exactly one side is a type variable and the concrete side is not
itself a union. Assignability uses the concrete type as both bounds,
while subtyping uses its gradual top and bottom materializations.
Materialized `Divergent` cycle markers are normalized before
constructing the range so the shortcut preserves the existing
cycle-recovery behavior.
### Fixed by this PR
The optimization applies when the outer union contains invariant
specializations whose invariant arguments are concrete, non-union types.
For example:
```python
class A: ...
class B: ...
Results = dict[int, A] | dict[int, B]
Rows = list[tuple[int, A]] | list[tuple[int, B]]
def map_rows[T](rows: list[tuple[int, T]]) -> dict[int, T]: ...
def perform(rows: Rows) -> Results:
return map_rows(rows)
```
The issue's 21-arm version of this pattern drops from 14.29 seconds on
`main` to 0.04 seconds with this change.
### Not fixed by this PR
The shortcut deliberately does not apply when the invariant argument is
itself a union. For example:
```python
class A0: ...
class B0: ...
class A1: ...
class B1: ...
Results = dict[int, A0 | B0] | dict[int, A1 | B1]
Rows = list[tuple[int, A0 | B0]] | list[tuple[int, A1 | B1]]
def map_rows[T](rows: list[tuple[int, T]]) -> dict[int, T]: ...
def perform(rows: Rows) -> Results:
return map_rows(rows)
```
A large version of this nested-union pattern remains pathological. These
bounds stay on the structural relation path because storing an entire
union as one exact bound regressed pandas-stubs, pydantic, scikit-learn,
colour, and manticore. Fixing that residual case requires a separate
optimization that preserves union distribution and negative-path
semantics.
With union distribution preserved, the affected ecosystem projects
return to base-level runtime while the original issue reproduction
remains fast. The Criterion suite benchmarks both the optimized pattern
and the union-bound regression guard.
Closes https://github.com/astral-sh/ty/issues/3896.