ruff/crates/ty_python_semantic/resources/mdtest/narrow/while.md
David Peter db3dcd8ad6
[ty] Eagerly simplify 'True' and 'False' constraints (#18998)
## Summary

Simplifies literal `True` and `False` conditions to `ALWAYS_TRUE` /
`ALWAYS_FALSE` during semantic index building. This allows us to eagerly
evaluate more constraints, which should help with performance (looks
like there is a tiny 1% improvement in instrumented benchmarks), but
also allows us to eliminate definitely-unreachable branches in
control-flow merging. This can lead to better type inference in some
cases because it allows us to retain narrowing constraints without
solving https://github.com/astral-sh/ty/issues/690 first:
```py
def _(c: int | None):
    if c is None:
        assert False
    
    reveal_type(c)  # int, previously: int | None
```

closes https://github.com/astral-sh/ty/issues/713

## Test Plan

* Regression test for https://github.com/astral-sh/ty/issues/713
* Made sure that all ecosystem diffs trace back to removed false
positives
2025-06-30 13:11:52 +02:00

1.4 KiB

Narrowing in while loops

We only make sure that narrowing works for while loops in general, we do not exhaustively test all narrowing forms here, as they are covered in other tests.

Note how type narrowing works subtly different from if ... else, because the negated constraint is retained after the loop.

Basic while loop

def next_item() -> int | None:
    return 1

x = next_item()

while x is not None:
    reveal_type(x)  # revealed: int
    x = next_item()

reveal_type(x)  # revealed: None

while loop with else

def next_item() -> int | None:
    return 1

x = next_item()

while x is not None:
    reveal_type(x)  # revealed: int
    x = next_item()
else:
    reveal_type(x)  # revealed: None

reveal_type(x)  # revealed: None

Nested while loops

from typing import Literal

def next_item() -> Literal[1, 2, 3]:
    raise NotImplementedError

x = next_item()

while x != 1:
    reveal_type(x)  # revealed: Literal[2, 3]

    while x != 2:
        # TODO: this should be Literal[1, 3]; Literal[3] is only correct
        # in the first loop iteration
        reveal_type(x)  # revealed: Literal[3]
        x = next_item()

    x = next_item()

With break statements

def next_item() -> int | None:
    return 1

while True:
    x = next_item()
    if x is not None:
        break

reveal_type(x)  # revealed: int