Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 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
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
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 @@ -180,14 +180,26 @@ 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:
Expand All @@ -198,9 +210,14 @@ def _(x: int | 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
```

## Narrowing from `assert` should not affect reassigned variables
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