Skip to content

exponential runtime for some large unions #1968

@markjm

Description

@markjm

Summary

Hi all - thank you so much for this promising new project! I am excited to get our company using ty ASAP. With the beta announcement today, I tried it out on our JUMBO codebase. In doing so, I have identified the below issue:

Included here is a minimal repro from our codebase (reduced as much as possible and business logic filtered out, i realize it looks silly but i figure it demonstrated the issue well):

"""
Minimal reproduction for ty type checker exponential slowdown.

Run with:
    TY_MAX_PARALLELISM=1 ty check slow_ty_repro.py

Timings:
    4 types: <1s
    5 types: <1s
    6 types: ~8s
    7 types: >60s (hits my timeout)

Example debug logs:
2025-12-17 01:33:38.681798202 DEBUG left implies right left=(M6 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) right=(U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements)
2025-12-17 01:33:38.682086278 DEBUG left and right overlap left=(M6 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) right=(U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) intersection=(M6 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements)
2025-12-17 01:33:38.683285882 DEBUG left implies right left=(M3 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) right=(U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements)
2025-12-17 01:33:38.683605638 DEBUG left and right overlap left=(M3 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) right=(U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) intersection=(M3 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements)
2025-12-17 01:33:38.683894444 DEBUG left implies right left=(M3 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) right=(M3 ≤ U_co@apply)
2025-12-17 01:33:38.683919264 DEBUG left and right overlap left=(M3 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements) right=(M3 ≤ U_co@apply) intersection=(M3 ≤ U_co@apply ≤ M1 | M2 | M3 | ... omitted 4 union elements)

"""

from __future__ import annotations

from typing import Callable
from typing import Generic
from typing import TypeVar
from typing import Union


class M1: pass
class M2: pass
class M3: pass
class M4: pass
class M5: pass
class M6: pass
class M7: pass

Msg = Union[M1, M2, M3, M4, M5, M6, M7]

T = TypeVar("T")
U_co = TypeVar("U_co", covariant=True)


class Stream(Generic[T]):
    def apply(self, func: Callable[["Stream[T]"], "Stream[U_co]"]) -> "Stream[U_co]":
        return func(self)


TMsg = TypeVar("TMsg", bound=Msg)


class Builder(Generic[TMsg]):
    def build(self) -> Stream[TMsg]:
        stream: Stream[TMsg] = Stream()
        stream = stream.apply(self._handler) # i think here is where the issue triggers?
        return stream

    def _handler(self, stream: Stream[Msg]) -> Stream[Msg]:
        return stream

this initially just surfaced as what seemed to be a hang during a repo-wide ty check, but debug logs allowed me to narrow down to this file then I just hacked away until i got something which seemed reasonable to share.

Version

0.0.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePotential performance improvement

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions