mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Avoid exponential narrowing of gradual string-literal unions (#27742)
## Summary Fix the exponential memory regression when equality narrowing encounters a union of `Any & Literal["..."]` alternatives. `evaluate_target_union` intersected each surviving alternative with the complement of every rejected alternative, even when the survivor and rejected union were already disjoint. For gradual string literals, expanding those redundant complements created exponentially many equivalent intersections. Skip the exclusions when disjointness is proven; preserve them for alternatives that can overlap, including `Any` and `TypeVar` cases. On the original 20-member reproducer, peak memory drops from **304.4 MiB to 38.8 MiB**; the reduced intersection-only reproducer drops from **301.4 MiB to 38.5 MiB**. The original 67-member case, previously exponential/OOM, now completes in **0.061 s using 42.0 MiB**. Closes astral-sh/ty#4256. ## Test plan - Added mdtests covering both branches of `==` and `!=` for gradual string-literal unions, plus a 20-member regression that exercises the original exponential expansion. - Added the dedicated `ty_micro[gradual_literal_union_equality]` CodSpeed microbenchmark for the 20-member case. - Ran all 481 ty semantic mdtests and the repository hooks.
This commit is contained in:
1 parent
083ec1e307
commit
bf15c8cfcc
3 files changed
+94
-1
No files matched your search
@@ -1254,6 +1254,39 @@ fn benchmark_literal_equality_fallthrough_guarded_any(criterion: &mut Criterion)
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression benchmark for <https://github.com/astral-sh/ty/issues/4256>.
|
||||
///
|
||||
/// Excluding rejected gradual string literals must not expand the complement of each intersection
|
||||
/// into exponentially many equivalent alternatives.
|
||||
fn benchmark_gradual_literal_union_equality(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
let mut code = String::from(
|
||||
"from typing import Any, Literal\nfrom ty_extensions import Intersection\n\ndef check(value: (\n",
|
||||
);
|
||||
for index in 0..20 {
|
||||
writeln!(
|
||||
&mut code,
|
||||
" {}Intersection[Any, Literal[\"{index}\"]]",
|
||||
if index == 0 { "" } else { "| " },
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
code.push_str(")) -> None:\n assert value == \"0\"\n repr(value)\n");
|
||||
|
||||
criterion.bench_function("ty_micro[gradual_literal_union_equality]", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| setup_micro_case(&code),
|
||||
|case| {
|
||||
let Case { db } = case;
|
||||
let result = db.check();
|
||||
assert_eq!(result.len(), 0);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Regression benchmark for <https://github.com/astral-sh/ty/issues/3880>.
|
||||
///
|
||||
/// Reachability analysis for a large literal OR pattern on `Any` used to rebuild the remaining
|
||||
@@ -1582,6 +1615,7 @@ criterion_group!(
|
||||
benchmark_literal_match_fallthrough,
|
||||
benchmark_literal_match_fallthrough_guarded_any,
|
||||
benchmark_literal_equality_fallthrough_guarded_any,
|
||||
benchmark_gradual_literal_union_equality,
|
||||
benchmark_literal_or_pattern_reachability,
|
||||
benchmark_typeis_narrowing,
|
||||
benchmark_repeated_statement_calls,
|
||||
|
||||
@@ -2184,6 +2184,59 @@ def gradual_enum_union_inequality(value: Color | Any, other: Color):
|
||||
reveal_type(value) # revealed: Color | Any
|
||||
```
|
||||
|
||||
## Unions of gradual string literals
|
||||
|
||||
Comparing a union of string literals intersected with `Any` keeps the matching alternative for
|
||||
equality and removes it for inequality:
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
from ty_extensions import Intersection
|
||||
|
||||
def equality(value: Intersection[Any, Literal["a"]] | Intersection[Any, Literal["b"]]):
|
||||
if value == "a":
|
||||
reveal_type(value) # revealed: Any & Literal["a"]
|
||||
else:
|
||||
reveal_type(value) # revealed: Any & Literal["b"]
|
||||
|
||||
if value != "a":
|
||||
reveal_type(value) # revealed: Any & Literal["b"]
|
||||
else:
|
||||
reveal_type(value) # revealed: Any & Literal["a"]
|
||||
```
|
||||
|
||||
Larger unions must narrow without expanding the complement of every rejected alternative, which
|
||||
would make memory use grow exponentially:
|
||||
|
||||
```py
|
||||
def larger_union(
|
||||
value: (
|
||||
Intersection[Any, Literal["a"]]
|
||||
| Intersection[Any, Literal["b"]]
|
||||
| Intersection[Any, Literal["c"]]
|
||||
| Intersection[Any, Literal["d"]]
|
||||
| Intersection[Any, Literal["e"]]
|
||||
| Intersection[Any, Literal["f"]]
|
||||
| Intersection[Any, Literal["g"]]
|
||||
| Intersection[Any, Literal["h"]]
|
||||
| Intersection[Any, Literal["i"]]
|
||||
| Intersection[Any, Literal["j"]]
|
||||
| Intersection[Any, Literal["k"]]
|
||||
| Intersection[Any, Literal["l"]]
|
||||
| Intersection[Any, Literal["m"]]
|
||||
| Intersection[Any, Literal["n"]]
|
||||
| Intersection[Any, Literal["o"]]
|
||||
| Intersection[Any, Literal["p"]]
|
||||
| Intersection[Any, Literal["q"]]
|
||||
| Intersection[Any, Literal["r"]]
|
||||
| Intersection[Any, Literal["s"]]
|
||||
| Intersection[Any, Literal["t"]]
|
||||
),
|
||||
):
|
||||
if value == "a":
|
||||
reveal_type(value) # revealed: Any & Literal["a"]
|
||||
```
|
||||
|
||||
## Booleans and integers
|
||||
|
||||
```py
|
||||
|
||||
@@ -1166,7 +1166,13 @@ fn evaluate_target_union<'db>(
|
||||
let Some(mut narrowed) = narrowed else {
|
||||
continue;
|
||||
};
|
||||
if let Some(removed) = removed {
|
||||
// A surviving alternative that is disjoint from every rejected alternative already
|
||||
// satisfies their exclusions. Constructing those redundant exclusions can exponentially
|
||||
// expand intersections such as `Any & Literal["a"]` when the rejected alternatives are
|
||||
// similarly shaped intersections with other string literals.
|
||||
if let Some(removed) = removed
|
||||
&& !narrowed.is_disjoint_from(db, env, removed)
|
||||
{
|
||||
narrowed = IntersectionBuilder::new(db, env)
|
||||
.add_positive(narrowed)
|
||||
.add_negative(removed)
|
||||
|
||||
Reference in new issue
Block a user