diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index faae0f88b5..f36a145da2 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -1254,6 +1254,39 @@ fn benchmark_literal_equality_fallthrough_guarded_any(criterion: &mut Criterion) ); } +/// Regression benchmark for . +/// +/// 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 . /// /// 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, diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 95cd22a1f8..0f15bf57d1 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -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 diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 5400a61036..92a7aeab69 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -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)