[ty] Treat non-empty range calls as non-empty for reachability#25220
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 94.47%. The percentage of expected errors that received a diagnostic held steady at 89.19%. The number of fully passing files held steady at 95/134. |
Memory usage reportSummary
Significant changesClick to expand detailed breakdownprefect
trio
sphinx
flake8
|
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
possibly-unresolved-reference |
0 | 86 | 0 |
unresolved-attribute |
0 | 8 | 0 |
division-by-zero |
0 | 1 | 0 |
not-iterable |
0 | 1 | 0 |
| Total | 0 | 96 | 0 |
Raw diff (96 changes)
cloud-init (https://github.com/canonical/cloud-init)
- cloudinit/distros/networking.py:163:65 warning[possibly-unresolved-reference] Name `missing` used when possibly not defined
- cloudinit/net/netplan.py:456:12 warning[possibly-unresolved-reference] Name `last_exception` used when possibly not defined
- cloudinit/net/netplan.py:460:20 warning[possibly-unresolved-reference] Name `last_exception` used when possibly not defined
- cloudinit/sources/DataSourceLXD.py:317:37 warning[possibly-unresolved-reference] Name `response` used when possibly not defined
- cloudinit/sources/DataSourceLXD.py:318:25 warning[possibly-unresolved-reference] Name `response` used when possibly not defined
- cloudinit/sources/DataSourceLXD.py:321:22 warning[possibly-unresolved-reference] Name `response` used when possibly not defined
- cloudinit/sources/DataSourceLXD.py:323:22 warning[possibly-unresolved-reference] Name `response` used when possibly not defined
- cloudinit/sources/DataSourceLXD.py:326:12 warning[possibly-unresolved-reference] Name `response` used when possibly not defined
colour (https://github.com/colour-science/colour)
- colour/models/osa_ucs.py:341:38 warning[possibly-unresolved-reference] Name `XYZ` used when possibly not defined
core (https://github.com/home-assistant/core)
- homeassistant/components/ws66i/__init__.py:52:41 warning[possibly-unresolved-reference] Name `amp_num` used when possibly not defined
- homeassistant/components/zha/radio_manager.py:468:59 warning[possibly-unresolved-reference] Name `retry` used when possibly not defined
cryptography (https://github.com/pyca/cryptography)
- tests/hazmat/primitives/test_x25519.py:82:16 warning[possibly-unresolved-reference] Name `computed_shared_key` used when possibly not defined
- tests/hazmat/primitives/test_x448.py:83:16 warning[possibly-unresolved-reference] Name `computed_shared_key` used when possibly not defined
dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ddtrace/vendor/psutil/_pswindows.py:720:32 warning[possibly-unresolved-reference] Name `err` used when possibly not defined
- tests/integration/test_sampling.py:336:46 warning[possibly-unresolved-reference] Name `start_time` used when possibly not defined
- tests/tracer/test_writer.py:255:43 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- tests/tracer/test_writer.py:255:55 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- tests/tracer/test_writer.py:280:51 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
ibis (https://github.com/ibis-project/ibis)
- ibis/expr/tests/test_api.py:190:12 error[unresolved-attribute] Attribute `name` is not defined on `Node` in union `Node | Unknown`
ignite (https://github.com/pytorch/ignite)
- tests/ignite/engine/test_memory_leaks.py:43:27 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- tests/ignite/handlers/test_param_scheduler.py:768:67 warning[possibly-unresolved-reference] Name `data` used when possibly not defined
- tests/ignite/handlers/test_param_scheduler.py:768:75 warning[possibly-unresolved-reference] Name `max_epochs` used when possibly not defined
- tests/ignite/handlers/test_param_scheduler.py:769:12 warning[possibly-unresolved-reference] Name `lrs1` used when possibly not defined
- tests/ignite/handlers/test_param_scheduler.py:770:12 warning[possibly-unresolved-reference] Name `lrs2` used when possibly not defined
materialize (https://github.com/MaterializeInc/materialize)
- test/kafka-low-watermark/mzcompose.py:80:61 warning[possibly-unresolved-reference] Name `low` used when possibly not defined
- test/kafka-low-watermark/mzcompose.py:80:73 warning[possibly-unresolved-reference] Name `high` used when possibly not defined
- test/0dt/mzcompose.py:2173:21 warning[possibly-unresolved-reference] Name `settled_anon` used when possibly not defined
- test/0dt/mzcompose.py:2177:12 warning[possibly-unresolved-reference] Name `settled_anon` used when possibly not defined
- test/canary-load/mzcompose.py:554:21 warning[possibly-unresolved-reference] Name `received_timestamps` used when possibly not defined
optuna (https://github.com/optuna/optuna)
- docs/visualization_matplotlib_examples/optuna.visualization.matplotlib.intermediate_values.py:37:12 warning[possibly-unresolved-reference] Name `y` used when possibly not defined
- docs/visualization_examples/optuna.visualization.plot_intermediate_values.py:38:12 warning[possibly-unresolved-reference] Name `y` used when possibly not defined
- tests/samplers_tests/tpe_tests/test_sampler.py:421:59 warning[possibly-unresolved-reference] Name `trial` used when possibly not defined
- tutorial/10_key_features/005_visualization.py:159:12 warning[possibly-unresolved-reference] Name `val_accuracy` used when possibly not defined
pandas (https://github.com/pandas-dev/pandas)
- pandas/tests/tseries/offsets/test_week.py:113:37 warning[possibly-unresolved-reference] Name `date` used when possibly not defined
- pandas/tests/tseries/offsets/test_week.py:113:43 warning[possibly-unresolved-reference] Name `expected` used when possibly not defined
pandera (https://github.com/pandera-dev/pandera)
- tests/fastapi/test_app.py:60:12 warning[possibly-unresolved-reference] Name `response` used when possibly not defined
prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-ray/tests/test_task_runners.py:91:40 warning[possibly-unresolved-reference] Name `result` used when possibly not defined
- src/integrations/prefect-ray/tests/test_task_runners.py:91:56 warning[possibly-unresolved-reference] Name `result` used when possibly not defined
- src/integrations/prefect-shell/tests/test_commands.py:38:12 warning[possibly-unresolved-reference] Name `logs` used when possibly not defined
psycopg (https://github.com/psycopg/psycopg)
- tests/test_waiting.py:142:78 warning[possibly-unresolved-reference] Name `the_ex` used when possibly not defined
- tests/test_waiting_async.py:150:78 warning[possibly-unresolved-reference] Name `the_ex` used when possibly not defined
- tests/types/test_string.py:265:76 warning[possibly-unresolved-reference] Name `obj` used when possibly not defined
- tests/types/test_string.py:265:82 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- tests/types/test_string.py:296:23 warning[possibly-unresolved-reference] Name `val` used when possibly not defined
pycryptodome (https://github.com/Legrandin/pycryptodome)
- lib/Crypto/SelfTest/Cipher/common.py:141:27 warning[possibly-unresolved-reference] Name `cipher` used when possibly not defined
- lib/Crypto/SelfTest/Cipher/common.py:143:13 warning[possibly-unresolved-reference] Name `decipher` used when possibly not defined
- lib/Crypto/SelfTest/Random/test_random.py:75:70 warning[possibly-unresolved-reference] Name `step` used when possibly not defined
- lib/Crypto/SelfTest/Random/test_random.py:76:69 warning[possibly-unresolved-reference] Name `step` used when possibly not defined
- lib/Crypto/SelfTest/Random/test_random.py:76:75 warning[possibly-unresolved-reference] Name `step` used when possibly not defined
- lib/Crypto/SelfTest/Random/test_random.py:78:67 warning[possibly-unresolved-reference] Name `step` used when possibly not defined
- lib/Crypto/SelfTest/Random/test_random.py:79:64 warning[possibly-unresolved-reference] Name `step` used when possibly not defined
- lib/Crypto/SelfTest/Random/test_random.py:90:67 warning[possibly-unresolved-reference] Name `step` used when possibly not defined
- lib/Crypto/PublicKey/ECC.py:1341:18 warning[possibly-unresolved-reference] Name `pointX` used when possibly not defined
- lib/Crypto/SelfTest/Hash/test_BLAKE2.py:390:30 warning[possibly-unresolved-reference] Name `h` used when possibly not defined
- lib/Crypto/SelfTest/Hash/test_BLAKE2.py:450:30 warning[possibly-unresolved-reference] Name `h` used when possibly not defined
- lib/Crypto/SelfTest/Hash/test_Poly1305.py:527:62 warning[possibly-unresolved-reference] Name `auth` used when possibly not defined
pytest (https://github.com/pytest-dev/pytest)
- testing/test_tmpdir.py:449:41 warning[possibly-unresolved-reference] Name `d` used when possibly not defined
pywin32 (https://github.com/mhammond/pywin32)
- com/win32com/test/testPyComTest.py:523:26 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
scipy (https://github.com/scipy/scipy)
- scipy/special/tests/test_ellip_harm.py:42:22 warning[possibly-unresolved-reference] Name `xsum` used when possibly not defined
- scipy/integrate/_bvp.py:482:13 warning[possibly-unresolved-reference] Name `y_new` used when possibly not defined
- scipy/integrate/_bvp.py:483:13 warning[possibly-unresolved-reference] Name `p_new` used when possibly not defined
- scipy/integrate/_bvp.py:495:20 warning[possibly-unresolved-reference] Name `step_new` used when possibly not defined
- scipy/integrate/_bvp.py:496:20 warning[possibly-unresolved-reference] Name `cost_new` used when possibly not defined
- scipy/integrate/_ivp/bdf.py:69:23 warning[possibly-unresolved-reference] Name `k` used when possibly not defined
- scipy/integrate/_ivp/radau.py:136:23 warning[possibly-unresolved-reference] Name `k` used when possibly not defined
- scipy/interpolate/_interpnd_info.py:23:11 error[unresolved-attribute] Attribute `diff` is not defined on `Literal[0]` in union `Literal[0] | Unknown`
- scipy/optimize/tests/test_chandrupatla.py:127:20 warning[possibly-unresolved-reference] Name `x3` used when possibly not defined
- scipy/optimize/tests/test_chandrupatla.py:127:32 warning[possibly-unresolved-reference] Name `f3` used when possibly not defined
- scipy/optimize/tests/test_optimize.py:726:25 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- scipy/signal/_signaltools.py:1243:9 warning[possibly-unresolved-reference] Name `number` used when possibly not defined
- scipy/signal/_signaltools.py:1247:18 warning[possibly-unresolved-reference] Name `number` used when possibly not defined
- scipy/special/tests/test_basic.py:4269:47 warning[possibly-unresolved-reference] Name `n` used when possibly not defined
- scipy/special/tests/test_basic.py:4279:47 warning[possibly-unresolved-reference] Name `n` used when possibly not defined
- scipy/stats/_distn_infrastructure.py:877:24 error[unresolved-attribute] Attribute `replace` is not defined on `None` in union `str | None`
- scipy/stats/tests/test_multivariate.py:2232:24 error[unresolved-attribute] Attribute `T` is not defined on `None` in union `None | ndarray[tuple[Any, ...], dtype[Unknown]] | ndarray[tuple[Any, ...], dtype[float64]]`
- scipy/stats/tests/test_multivariate.py:2234:23 error[not-iterable] Object of type `None | ndarray[tuple[Any, ...], dtype[Unknown]] | ndarray[tuple[Any, ...], dtype[float64]]` may not be iterable
sympy (https://github.com/sympy/sympy)
- sympy/core/tests/test_expand.py:248:9 error[unresolved-attribute] Attribute `expand` is not defined on `Literal[1]` in union `Literal[1] | Expr`
- sympy/stats/sampling/tests/test_sample_continuous_rv.py:121:20 warning[possibly-unresolved-reference] Name `X` used when possibly not defined
- sympy/stats/sampling/tests/test_sample_continuous_rv.py:123:24 warning[possibly-unresolved-reference] Name `X` used when possibly not defined
- sympy/core/evalf.py:1364:33 warning[possibly-unresolved-reference] Name `err` used when possibly not defined
- sympy/core/evalf.py:1365:40 warning[possibly-unresolved-reference] Name `s` used when possibly not defined
- sympy/core/numbers.py:172:16 warning[possibly-unresolved-reference] Name `ca` used when possibly not defined
- sympy/core/numbers.py:172:31 warning[possibly-unresolved-reference] Name `ca` used when possibly not defined
- sympy/core/numbers.py:173:55 warning[possibly-unresolved-reference] Name `ca` used when possibly not defined
- sympy/core/tests/test_relational.py:694:14 warning[possibly-unresolved-reference] Name `f` used when possibly not defined
- sympy/core/tests/test_relational.py:694:36 warning[possibly-unresolved-reference] Name `f` used when possibly not defined
- sympy/physics/quantum/cg.py:147:25 error[unresolved-attribute] Attribute `parens` is not defined on `None` in union `None | prettyForm`
- sympy/physics/quantum/cg.py:322:25 error[unresolved-attribute] Attribute `parens` is not defined on `None` in union `None | prettyForm`
- sympy/physics/quantum/cg.py:426:25 error[unresolved-attribute] Attribute `parens` is not defined on `None` in union `None | prettyForm`
urllib3 (https://github.com/urllib3/urllib3)
- test/test_collections.py:23:11 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- test/test_collections.py:23:24 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
- test/test_collections.py:27:17 warning[possibly-unresolved-reference] Name `i` used when possibly not defined
vision (https://github.com/pytorch/vision)
- test/test_ops.py:2449:24 warning[division-by-zero] Cannot divide object of type `Literal[0]` by zero
zulip (https://github.com/zulip/zulip)
- zerver/tests/test_external.py:130:21 warning[possibly-unresolved-reference] Name `result` used when possibly not defined
- zerver/tests/test_home.py:1012:23 warning[possibly-unresolved-reference] Name `defunct_user` used when possibly not defined
- zerver/tests/test_home.py:1020:26 warning[possibly-unresolved-reference] Name `defunct_user` used when possibly not defined0595810 to
da2e341
Compare
|
|
||
| /// Returns `true` if the literal arguments to `range(...)` guarantee at least one iteration. | ||
| /// | ||
| /// The check is intentionally syntactic and conservative: keywords, starred arguments, |
There was a problem hiding this comment.
Oh, I wasn't thinking we'd do a syntactic check — I was thinking we'd have a Type::KnownInstance(KnownInstanceType::Range{ is_positive: book }) type, and special-case the range constructor in type inference to produce that type if a literal integer was passed in. Then we could use the inner bool to inform how we resolve the reachability constraints later on during type-checking. I think that would be much more robust than a syntactic check
There was a problem hiding this comment.
I wasn't sure that'd be necessary if we weren't going to use it beyond reachability. I'll give it a shot though.
There was a problem hiding this comment.
The downside of this is that we're now creating a TDD node for every iterable, even if it's not a range 😭
There was a problem hiding this comment.
I ended up with a bit of a hybrid: use a KnownInstanceType, but still use a syntactic check to keep the reachability graph manageable.
fa08429 to
3105102
Compare
Merging this PR will improve performance by 5.25%
Performance Changes
Tip Curious why this is faster? Use the CodSpeed MCP and ask your agent. Comparing Footnotes
|
7938511 to
321551d
Compare
range calls as non-empty for reachability
9d3860e to
667a3fb
Compare
| /// | ||
| /// This avoids adding reachability predicates for every `for` loop target to the TDD graph. We only | ||
| /// emit the predicate for syntactically direct `range(...)` calls; type checking later verifies that | ||
| /// the callee resolves to the built-in `range` and determines whether the range is statically | ||
| /// non-empty. | ||
| fn is_direct_range_call(expr: &ast::Expr) -> bool { |
There was a problem hiding this comment.
We would obviously prefer to avoid this, but I'm having trouble doing so without causing a significant performance regression, since without this guard, we end up creating TDD nodes for every iterable (even if they end up not being range calls later on).
1b3dc8e to
9fb12f0
Compare
carljm
left a comment
There was a problem hiding this comment.
Looks generally OK but there is a correctness issue we need to fix. These kinds of special cases tend to feel like they'll be easy, but introduce more edge cases than you'd think. But I think the ecosystem diagnostics are good enough to make it worth doing.
It also feels like if we're doing this for range, we should at least also do it for empty/non-empty builtin collection literals, too? Not sure what the ecosystem impact of that would look like, but it would be significantly easier since it's detectable purely syntactically.
| /// call is `Unknown`/`Any`, because that would result in too many false | ||
| /// positives. | ||
| IsNonTerminalCall(CallableAndCallExpr<'db>), | ||
| /// A direct `range(...)` call used as a `for` iterable. |
There was a problem hiding this comment.
Nit: the name of this predicate is broader, and I could easily see us adding more cases to it in future, so I'd probably word this more generally and mention range as the only current actual case we handle.
| // A `range(0)` initializer can become the inferred target type for later assignments, | ||
| // but the emptiness refinement only applies to the original value expression. | ||
| (_, Type::KnownInstance(known_instance @ KnownInstanceType::Range { .. })) => { | ||
| self.check_type_pair(db, source, known_instance.instance_fallback(db)) | ||
| } |
There was a problem hiding this comment.
This makes empty range and non-empty range mutually subtypes of each other -- but the problem is that this means which one survives in a union order-dependent. In this example, we'll wrongly think the final block is unreachable, because the empty-range is the one that will survive in the union:
def f(flag: bool) -> None:
value = range(0)
if flag:
value = range(1)
if value:
reveal_type(value) # PR reveals Never; reachable when flag is TrueI think the most correct fix here is to make it so that empty and non-empty ranges are actually disjoint types, so not subtypes in either direction. I think this is correct and would work fine and solve the problem -- we just need to make sure these known-instance types get literal-promoted to the instance fallback type if you e.g. make a list like list[range(0)] you get list[range] and not list[EmptyRange]. We probably also would want special logic in UnionBuilder to unify EmptyRange | NonEmptyRange in a union to just range?
| } | ||
| KnownInstanceType::Range { .. } => f | ||
| .with_type(KnownClass::Range.to_class_literal(self.db)) | ||
| .write_str("range"), |
There was a problem hiding this comment.
I think this may prove to be confusing (having distinct non-equivalent types that we display identically), but we can try it and see how it works in practice.
|
Yeah I had a follow-up PR for non-empty builtins :) |
add7d4b to
96af416
Compare
|
One more issue here: it looks like we are wrongly treating two different empty ranges as equality-equivalent, so this wrongly treats the left = range(1)
right = range(2)
if left == right:
reveal_type(left) # range
else:
reveal_type(left) # PR: Never; base: rangeNote that we are also treating empty ranges that way, but in this case it is correct! All empty ranges are the same object and compare equal. We should add tests for both empty and non-empty ranges, and fix the non-empty case. |
|
Oops, thank you. |
|
Fixed in #26351. |
## Summary This fixes an equality-narrowing regression exposed by #25220. `KnownInstanceType::Range` distinguishes statically empty and non-empty ranges, but `Type::is_single_valued` previously treated both refinements as single-valued. As a result, two different non-empty ranges such as `range(1)` and `range(2)` were considered equality-equivalent and the `else` branch below was incorrectly unreachable: ```python left = range(1) right = range(2) if left == right: reveal_type(left) # range else: reveal_type(left) # range ``` All empty ranges compare equal, so this keeps the empty-range refinement single-valued. Non-empty ranges can contain different values, so this treats that refinement as multiple-valued and preserves both equality branches. Regression coverage checks both behaviors. The focused mdtest, full ty semantic suite, workspace Clippy, and repository hooks pass. Follow-up to #25220 (comment).
## Summary This extends the static reachability analysis for synchronous `for` loops from astral-sh#25220 to tuple, list, set, dictionary, string, and bytes literals whose emptiness is known syntactically. We record their static truthiness: a non-empty literal proves that the body executes at least once, while an empty literal marks the body unreachable and prevents its definitions from flowing past the loop. ```python for item in [1]: pass reveal_type(item) # Literal[1] value = 1 for _ in []: value = "unreachable" reveal_type(value) # Literal[1] ``` Starred elements and dictionary unpacking remain ambiguous when they are the only elements, while an explicit element still proves non-emptiness. Other iterables continue on the existing ambiguous path. Synchronous literals used in `async for` do not use this shortcut because they do not satisfy async iteration.
Summary
This PR productionizes one of @AlexWaygood's suggestions whereby we special-case
rangeto detect that certain loops must trigger at least one iteration, which in turn allows us to avoid false positives around "possibly unbound" variables defined within that loop.For example:
This PR handles two- and three-argument ranges like
range(1, 3)andrange(3, 0, -1), while leaving otheriterables and dynamic
range(count)loops on the existing possibly-empty path.