Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
c6ad1e8
[ty] improve #23109 TDD-based narrowing
mtshiba Feb 12, 2026
989268d
cache `narrow_by_constraint_inner`
mtshiba Feb 12, 2026
7f8379c
refactor
mtshiba Feb 12, 2026
f92287c
further `narrow_by_constraint_inner` optimization
mtshiba Feb 12, 2026
a0a5213
Revert "further `narrow_by_constraint_inner` optimization"
mtshiba Feb 12, 2026
6909252
improve constant calculations with `resolve_to_literal`
mtshiba Feb 12, 2026
0e2ca45
further `narrow_by_constraint_inner` optimization (take 2)
mtshiba Feb 12, 2026
ae801f7
Revert "improve constant calculations with `resolve_to_literal`"
mtshiba Feb 13, 2026
8369501
Revert "further `narrow_by_constraint_inner` optimization (take 2)"
mtshiba Feb 13, 2026
b2e63ce
Reapply "improve constant calculations with `resolve_to_literal`"
mtshiba Feb 13, 2026
c9aa05b
Merge branch 'main' into improve-23109
mtshiba Feb 13, 2026
946ea42
Update place_state.rs
mtshiba Feb 13, 2026
b45d185
`narrow_by_constraint` optimization (take 3)
mtshiba Feb 14, 2026
f10df50
Revert "`narrow_by_constraint` optimization (take 3)"
mtshiba Feb 14, 2026
de6d3c1
Reapply "`narrow_by_constraint` optimization (take 3)"
mtshiba Feb 14, 2026
4eb12c8
add `PlaceVersion` to prevent the old shadowed narrowing constraint f…
mtshiba Feb 14, 2026
19e63c4
`narrow_by_constraint` optimization using `PlaceVersion`
mtshiba Feb 15, 2026
50fc1e9
`narrow_by_constraint` optimization using `UnionType::from_elements_w…
mtshiba Feb 15, 2026
cfa026c
Revert "`narrow_by_constraint` optimization using `UnionType::from_el…
mtshiba Feb 15, 2026
41059e9
optimization in `PredicatePlaceVersionInfo`
mtshiba Feb 15, 2026
cd6c0ef
remove `ReturnsNever` special casing
mtshiba Feb 15, 2026
8468a9e
remove `all_negative_narrowing_constraints_for_{expression, pattern}`
mtshiba Feb 15, 2026
c2fe06e
compact `PredicatePlaceVersions`
mtshiba Feb 15, 2026
6da3546
Revert "compact `PredicatePlaceVersions`"
mtshiba Feb 15, 2026
3c0be32
store place versions per definition in `UseDefMap`
mtshiba Feb 15, 2026
9142acd
remove `latest_place_version` from `Bindings`
mtshiba Feb 15, 2026
b0add5e
intern `bindings_by_use`
mtshiba Feb 15, 2026
fd02b2b
Revert "intern `bindings_by_use`"
mtshiba Feb 16, 2026
2938e78
follow review
mtshiba Feb 16, 2026
986a678
Merge branch 'main' into improve-23109
mtshiba Feb 16, 2026
432e7d4
Update narrow.rs
mtshiba Feb 16, 2026
8bff224
fix `L/RShift` implemenation in `resolve_to_literal`
mtshiba Feb 16, 2026
a3eb0ab
add unit tests for `resolve_to_literal`
mtshiba Feb 16, 2026
04dfb49
Revert "remove `latest_place_version` from `Bindings`"
mtshiba Feb 17, 2026
c002ce6
remove unnecessary code
mtshiba Feb 17, 2026
99ad21f
Merge branch 'main' into improve-23109
mtshiba Feb 17, 2026
65bdd1c
remove unnecessary code
mtshiba Feb 17, 2026
c1c0757
simplify `narrow_by_constraint_inner` logic
mtshiba Feb 17, 2026
3edbfdf
reduce redundancy checks in `narrow_by_constraint`
mtshiba Feb 17, 2026
56dd816
reduce redundancy checks in `narrow_by_constraint` (2)
mtshiba Feb 18, 2026
3e05314
memorize all return values in `narrow_by_constraint_inner`
mtshiba Feb 18, 2026
6b11b17
narrowing constraint id canonicalization
mtshiba Feb 18, 2026
c171e96
[ty] Propagate narrowing through always-true if-conditions
mtshiba Feb 18, 2026
fdd4d9f
Revert "[ty] Propagate narrowing through always-true if-conditions"
mtshiba Feb 18, 2026
4b1398c
Update reachability_constraints.rs
mtshiba Feb 18, 2026
5d984f4
disable non-effective cache
mtshiba Feb 18, 2026
f755755
Revert "disable non-effective cache"
mtshiba Feb 18, 2026
7e82288
disable non-effective cache (take 2)
mtshiba Feb 18, 2026
56c5011
Revert "disable non-effective cache (take 2)"
mtshiba Feb 18, 2026
738cf8d
cache two types intersection as a tracked function
mtshiba Feb 18, 2026
a1e303e
`NarrowingConstraint` has `Cunjunction`s
mtshiba Feb 18, 2026
2c8bade
add `GatedNarrowingConstraint`
mtshiba Feb 18, 2026
5b6f7f2
intern `NarrowingConstraint`
mtshiba Feb 19, 2026
3d60f3f
Merge branch 'main' into improve-23109
mtshiba Feb 19, 2026
a596400
revert `add `GatedNarrowingConstraint``
mtshiba Feb 19, 2026
fb05c74
cache two types union as a tracked function
mtshiba Feb 19, 2026
f476d36
Revert "cache two types union as a tracked function"
mtshiba Feb 19, 2026
50c976c
cache two types intersection as a tracked function (take 2)
mtshiba Feb 19, 2026
adb4472
use `IntersectionType::from_two_elements` more
mtshiba Feb 19, 2026
5c321a0
add fast path for intersection type redundancy check
mtshiba Feb 20, 2026
9363927
avoid expensive intersection type checks
mtshiba Feb 20, 2026
1c8120b
add benchmark for large union type narrowing
mtshiba Feb 20, 2026
71f41f1
Merge branch 'main' into improve-23109
mtshiba Feb 20, 2026
50b9866
Update post_if_statement.md
mtshiba Feb 20, 2026
64d2a86
remove unnecessary code
mtshiba Feb 20, 2026
c913247
Revert "remove unnecessary code"
mtshiba Feb 20, 2026
0030056
add `MAX_NARROWING_GATING_MERGES`
mtshiba Feb 20, 2026
9623224
Revert "add `MAX_NARROWING_GATING_MERGES`"
mtshiba Feb 20, 2026
a82538c
more aggressive short circuit in `Bindings::merge`
mtshiba Feb 20, 2026
52c4104
optimize `narrow_by_constraint_inner`
mtshiba Feb 20, 2026
27e5778
Merge branch 'main' into improve-23109
mtshiba Feb 20, 2026
736aeeb
experiment: revert "optimize `narrow_by_constraint_inner`"
mtshiba Feb 20, 2026
4f2ce84
Revert "experiment: revert "optimize `narrow_by_constraint_inner`""
mtshiba Feb 20, 2026
89eb8f9
tracked `evaluate_constraint_type`
mtshiba Feb 21, 2026
f251ccb
Revert "tracked `evaluate_constraint_type`"
mtshiba Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions crates/ruff_benchmark/benches/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,70 @@ class E(Enum):
});
}

/// Benchmark for narrowing a large union type through multiple match statements.
///
/// This is extracted from egglog-python's `pretty.py`, where a ~30-class union type
/// (`AllDecls`) is narrowed by exhaustive match statements.
///
/// Sample code structure:
/// ```python
/// from __future__ import annotations
/// from dataclasses import dataclass
///
/// @dataclass
/// class C0:
/// value: int
/// ...
///
/// AllDecls = C0 | C1 | ...
///
/// def process(decl: AllDecls) -> None:
/// match decl:
/// case C0(): pass
/// ...
/// case _: pass
/// ```
fn benchmark_large_union_narrowing(criterion: &mut Criterion) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this benchmark currently shows up as "new" in the codspeed report, so we can't see how much this PR improves performance on this benchmark relative to main. It might be better to add this benchmark in a standalone PR, wait for codspeed to finish on that PR branch, and then rebase this PR on top of that PR branch. Then Codspeed should tell us how much this PR improves performance on this benchmark.

const NUM_CLASSES: usize = 30;
const NUM_MATCH_BRANCHES: usize = 29;

setup_rayon();

let mut code =
"from __future__ import annotations\nfrom dataclasses import dataclass\n\n".to_string();

for i in 0..NUM_CLASSES {
writeln!(&mut code, "@dataclass\nclass C{i}:\n value: int\n").ok();
}

code.push_str("AllDecls = ");
for i in 0..NUM_CLASSES {
if i > 0 {
code.push_str(" | ");
}
write!(&mut code, "C{i}").ok();
}
code.push_str("\n\n");

code.push_str("def process(decl: AllDecls) -> None:\n match decl:\n");
for i in 0..NUM_MATCH_BRANCHES {
writeln!(&mut code, " case C{i}():\n pass",).ok();
}
code.push_str(" case _:\n pass\n\n");

criterion.bench_function("ty_micro[large_union_narrowing]", |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,
);
});
}

struct ProjectBenchmark<'a> {
project: InstalledProject<'a>,
fs: MemoryFileSystem,
Expand Down Expand Up @@ -820,6 +884,7 @@ criterion_group!(
benchmark_many_enum_members,
benchmark_many_enum_members_2,
benchmark_very_large_tuple,
benchmark_large_union_narrowing,
);
criterion_group!(project, anyio, attrs, hydra, datetype);
criterion_main!(check_file, micro, project);
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Binary operations on integers

> Developer's note: This is mainly a test for the behavior of the type inferer. The constant
> evaluator (`resolve_to_literal`) of `SemanticIndexBuilder` is implemented separately from the type
> inferer, so if you modify the contents of this file or the type inferer, please also modify the
> implementation of `resolve_to_literal` and the unit tests (semantic_index/tests/const_eval\_\*) at
> the same time.

## Basic Arithmetic

```py
Expand Down
42 changes: 33 additions & 9 deletions crates/ty_python_semantic/resources/mdtest/loops/while_loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,21 +247,45 @@ Here the loop condition forces `x` to be `False` at loop exit, because there is
def random() -> bool:
return True

x = random()
reveal_type(x) # revealed: bool
while x:
pass
reveal_type(x) # revealed: Literal[False]
def _(x: bool):
while x:
pass
reveal_type(x) # revealed: Literal[False]
```

However, we can't narrow `x` like this when there's a `break` in the loop:

```py
x = random()
while x:
if random():
def _(x: bool):
while x:
if random():
break
reveal_type(x) # revealed: bool

def _(x: bool):
while x:
pass
reveal_type(x) # revealed: Literal[False]

x = random()
while x:
if random():
break
reveal_type(x) # revealed: bool

def _(y: int | None):
x = 1
while True:
if x == 0:
break

if y is None:
y = 0
continue

break
reveal_type(x) # revealed: bool

reveal_type(y) # revealed: int
```

### Non-static loop conditions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def _(x: Literal["foo", b"bar"] | int):
pass
case b"bar" if reveal_type(x): # revealed: Literal[b"bar"] | int
pass
case _ if reveal_type(x): # revealed: int | Literal["foo", b"bar"]
case _ if reveal_type(x): # revealed: Literal["foo", b"bar"] | int
pass
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,27 +180,56 @@ def _(x: int | None):
```

```py
from typing import Final

def _(x: int | None):
if 1 + 1 == 2:
if x is None:
return
reveal_type(x) # revealed: int

# TODO: should be `int` (the else-branch of `1 + 1 == 2` is unreachable)
reveal_type(x) # revealed: int | None
reveal_type(x) # revealed: int

# non-constant but always-true condition
needs_inference: Final = True

def _(x: int | None):
if needs_inference:
if x is None:
return
reveal_type(x) # revealed: int

reveal_type(x) # revealed: int
```

This also works when the always-true condition is nested inside a narrowing branch:

```py
from typing import Literal

def _(x: int | None):
if x is None:
if 1 + 1 == 2:
return

# TODO: should be `int` (the inner always-true branch makes the outer
# if-branch terminal)
reveal_type(x) # revealed: int | None
reveal_type(x) # revealed: int

def _(x: int | None):
if x is None:
if needs_inference:
return

reveal_type(x) # revealed: int

def always_true(val: object) -> Literal[True]:
return True

def _(x: int | None):
if x is None:
if always_true(x):
return

reveal_type(x) # revealed: int
```

## Narrowing from `assert` should not affect reassigned variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ else:
reveal_type(x) # revealed: Never

if x or not x:
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, 0, "foo", "", b"bar", b""] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never

if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, 0, "foo", "", b"bar", b""] | bool | None | tuple[()]

if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1, True, "foo"]
Expand Down
109 changes: 109 additions & 0 deletions crates/ty_python_semantic/src/semantic_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,50 @@ mod tests {
.collect()
}

/// A function to test how the constant evaluator of `SemanticIndexBuilder` evaluates an expression
/// (the evaluation should match that of `TypeInferenceBuilder`).
/// For example, for the input `x = 1\nif cond: x = 2\nx`, if `cond` evaluates to `AlwaysTrue`, it returns `vec![2]`,
/// if it evaluates to `AlwaysFalse`, it returns `vec![1]`, ​​if it evaluates to `Ambiguous`, it returns `vec![1, 2]`.
fn reachable_bindings_for_terminal_use(content: &str) -> Vec<i64> {
let TestCase { db, file } = test_case(content);
let scope = global_scope(&db, file);
let module = parsed_module(&db, file).load(&db);
let ast = module.syntax();

let terminal_expr = ast
.body
.last()
.and_then(ast::Stmt::as_expr_stmt)
.map(|stmt| stmt.value.as_ref())
.expect("expected terminal expression statement");
let terminal_name = terminal_expr
.as_name_expr()
.expect("terminal expression should be a name");

let use_id = terminal_name.scoped_use_id(&db, scope);
let use_def = use_def_map(&db, scope);

use_def
.bindings_at_use(use_id)
.filter_map(|binding_with_constraints| {
let definition = binding_with_constraints.binding.definition()?;
let DefinitionKind::Assignment(assignment) = definition.kind(&db) else {
return None;
};

let ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(value),
..
}) = assignment.value(&module)
else {
return None;
};

value.as_i64()
})
.collect::<Vec<_>>()
}

#[test]
fn empty() {
let TestCase { db, file } = test_case("");
Expand Down Expand Up @@ -1590,6 +1634,71 @@ class C[T]:
assert_eq!(*num, 1);
}

#[test]
fn const_eval_lshift_overflow_is_ambiguous() {
let values = reachable_bindings_for_terminal_use(
"
x = 1
if 1 << 63:
x = 2
x
",
);
assert_eq!(values, vec![1, 2]);
}

#[test]
fn const_eval_lshift_zero_short_circuit() {
let values = reachable_bindings_for_terminal_use(
"
x = 1
if 0 << 4000000000000000000:
x = 2
x
",
);
assert_eq!(values, vec![1]);
}

#[test]
fn const_eval_rshift_large_positive() {
let values = reachable_bindings_for_terminal_use(
"
x = 1
if 1 >> 5000000000:
x = 2
x
",
);
assert_eq!(values, vec![1]);
}

#[test]
fn const_eval_rshift_large_negative_operand() {
let values = reachable_bindings_for_terminal_use(
"
x = 1
if (-1) >> 5000000000:
x = 2
x
",
);
assert_eq!(values, vec![2]);
}

#[test]
fn const_eval_negative_lshift_is_ambiguous() {
let values = reachable_bindings_for_terminal_use(
"
x = 1
if 42 << -3:
x = 2
x
",
);
assert_eq!(values, vec![1, 2]);
}

#[test]
fn expression_scope() {
let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4");
Expand Down
Loading