mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
When ty checks a value assignment such as `Alias = A[int] | B[int] | C[int]`, it currently normalizes `A[int] | B[int]` and then compares those same members again while building the final union. Longer chains repeat that work for every prefix, even when nothing ever uses the value as a type. Instead, this PR just stores the runtime operands of `|` in the runtime-union object, and only builds and normalizes the actual type union lazily, if or when the runtime union value needs to be understood as a type. For example, checking a stored union against `TypeForm[Base]` walks the operands once and caches the result. Checks such as `isinstance` can still inspect the original operands. This is an alternative to #29304, which keeps the type of every intermediate union available. For `(A | B) | C`, it normalizes `A | B`, then seeds a fresh `UnionBuilder` with those members. If the construction policy and inference context match, the builder only needs to compare `C` with the existing members. This PR stores the `A | B` object as the left operand of the outer union. If only the final type is requested, one union builder visits `A`, `B`, and `C`. A second request for that same union uses the cached type. The two approaches take about the same time when a 256-class union is consumed once. - For a chain of assignments like `U2 = U1 | Literal[2]`, each assignment provides a new type-variable binding context. In #29304 that context is part of the cache key, so these repeated assignments defeat the caching, and extending the union can revisit its prefixes even when nothing ever asks for their types. This accounts for the slowness of #29304 on the "Extend a literal union across 1024 assignments" case below. - If `Prefix` is a long union and `(Prefix | X, Prefix | Y, ...)` is checked against `tuple[TypeForm[Base], ...]`, every extension needs its type. #29304 can reuse the normalized `Prefix` across those extensions. This PR walks all its members for each distinct result. With 32 extensions, #29304 is nearly twice as fast. If this case turns out to occur commonly in practice, we could add intermediate prefix caching as well. This PR is larger than #29304, which leaves the runtime union representation largely intact and adds a path in `UnionBuilder` for reconstructing an already normalized prefix. This PR needs extra complexity in `UnionTypeInstance`, where specialization, cycle recovery, and runtime checks need to distinguish the original operands from the denoted type. But I prefer keeping the complexity of these runtime-unions in the runtime-union implementation, rather than allowing it to leak into the core `UnionBuilder`, as it does in #29304. This also fixes a bug (as seen in xarray in the ecosystem report) with a runtime check list `isinstance(x, float | int)`. Previously we would simplify this union, eliminating `int`, but `int` and `float` are separate classes at runtime, so we would then miss that the `isinstance` check catches integers as well. Now the `isinstance` narrowing can look at the original runtime operands instead. ## Test plan I timed optimized builds on Apple silicon using the same main revision (`157caa55a5`) for all three. The #29304 build applies its reviewed patch (`a0f998e2b4`) to that base. Each entry is the median elapsed time in milliseconds for six fresh, single-worker CLI checks with warm filesystem caches. | Workload | Main | #29304 | This change | | --- | ---: | ---: | ---: | | Construct an unused union of 256 specialized classes | 326 | 56 | 34 | | Check the stored union against `TypeForm[CommonBase]` | 328 | 57 | 60 | | Extend a literal union across 1,024 assignments | 46 | 3,789 | 29 | | Extend a union across 128 assignments and consume each prefix | 77 | 95 | 79 | | Check 32 extensions of the same 128-class prefix as type forms | 112 | 57 | 105 | ## Ecosystem report At `dcc4ce9890`, xarray loses two false positives because `isinstance` now recognizes integer chunk sizes. Three existing xarray diagnostics include the types that actually reach those branches. Two truthiness warnings in aioredis and dd-trace-py display the same union members in a different order. All seven changes are expected. [Full ecosystem report](https://github.com/astral-sh/ruff/pull/29727#issuecomment-6422364992)
Ruff Benchmarks
The ruff_benchmark crate benchmarks the linter and the formatter on individual files:
# Run once on the "baseline".
cargo bench -p ruff_benchmark -- --save-baseline=main
# Compare against the "baseline".
cargo bench -p ruff_benchmark -- --baseline=main
# Run the lexer benchmarks.
cargo bench -p ruff_benchmark lexer -- --baseline=main
See CONTRIBUTING.md on how to use these benchmarks.