Skip to content

[ty] Completely remove the NoReturn shortcut optimization (episode 2)#23994

Merged
sharkdp merged 1 commit intomainfrom
david/retry-remove-noreturn-opt
Mar 16, 2026
Merged

[ty] Completely remove the NoReturn shortcut optimization (episode 2)#23994
sharkdp merged 1 commit intomainfrom
david/retry-remove-noreturn-opt

Conversation

@sharkdp
Copy link
Copy Markdown
Contributor

@sharkdp sharkdp commented Mar 16, 2026

Summary

Re-apply #23378, this time without a semantic merge conflict.

Function calls are relevant for control flow analysis because they may
return `Never`/`NoReturn`, and can therefore be terminal. In order to
support this, we record `ReturnsNever(…)` constraints during semantic
index building for statement-level calls (in almost all situations).
These constraints keep track of the call expression such that they can
be evaluated during reachability analysis and type narrowing.

For example, if we see a call `f(a, b, c)`, we keep track of this full
call expression. The return type could depend on the arguments
(overloads, generics), so to determine if a call is terminal, we need to
evaluate the full call expression.

For performance reasons, this analysis contained a short-cut where we
looked at the return type annotation of the invoked callable (`f`).
Under certain conditions, we could immediately see that a call would
definitely be terminal. This previously helped with performance, but is
now apparently detrimental.

The optimization recently caused problems for generic function calls, so
we had to exclude those. It now turns out there was another bug, as I
figured out by looking at the ecosystem results on this PR. When the
callable expression could not be upcasted to a callable type, we assumed
that the call would never be terminal. However, if the callable itself
is `Never`, that should also be considered a terminal call. This can
happen in unreachable code. Consider this rather weird case, that I
extracted from a hydpy ecosystem hit:

```py
from typing import Never, Literal

def fail() -> Never:
    raise

def _(x: Literal["a", "b"]):
    if x == "a":
        if 1 + 1 == 2:
            return
        fail()
    if x == "b":
        return

    reveal_type(x)
```

On `main`, the revealed type of `x` is `Literal["a"]`, which is wrong.
Since the `fail()` call itself happens in unreachable code, we infer
`Never` for `fail` itself, and therefore considered the `if x == "a"`
branch to *not* be terminal. On this branch, that bug is fixed and we
correctly reveal `Never` for the type of `x`.

So the idea here is to get rid of this optimization all together and to
simply evaluate the full call expression in all cases, which also makes
this much easier to reason about.

(I find the `AlwaysFalse`/`AlwaysTrue`-answer of this evaluation very
rather confusing, because it's sort of negated twice; I plan to change
this in a follow-up)

We previously merged #19867 to
address [a performance
problem](astral-sh/ty#968) in this part of the
codebase. It seems to me that the original problem was related to the
short-cut path, but I'm not sure if this could bring back the lock
congestion problem.

In any case, this PR seems to be a performance win across the board on
our usual benchmarks. And there is also a small decrease in memory
usage.

<img width="818" height="818" alt="image"
src="https://github.com/user-attachments/assets/b5b041ca-9e06-472d-8a5a-348ce29e2433"
/>

The disappearing diagnostics all seem to be related to cases like above,
where the terminal call happened in unreachable code.

Existing tests.
@sharkdp sharkdp added ty Multi-file analysis & type inference internal An internal refactor or improvement labels Mar 16, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 16, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 85.29%. The percentage of expected errors that received a diagnostic held steady at 78.13%. The number of fully passing files held steady at 64/132.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 16, 2026

mypy_primer results

Changes were detected when running on open source projects
pip (https://github.com/pypa/pip)
- src/pip/_vendor/packaging/version.py:48:24: error[call-non-callable] Object of type `object` is not callable
- Found 656 diagnostics
+ Found 655 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | ((dict[str, int | float | str | ... omitted 3 union elements], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
+ pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | ((dict[str, Divergent], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
- pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `dict[str, int | float | str | ... omitted 3 union elements] | dict[Never, Never] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `dict[str, Divergent] | dict[Never, Never] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
- pydantic/v1/utils.py:613:16: error[invalid-return-type] Return type does not match returned value: expected `Mapping[int | str, Any]`, found `(AbstractSet[int | str] & Top[Mapping[Unknown, object]]) | (Mapping[int | str, Any] & AbstractSet[object]) | (Mapping[int | str, Any] & ~AbstractSet[object]) | dict[int | str, ellipsis]`
+ pydantic/v1/utils.py:613:16: error[invalid-return-type] Return type does not match returned value: expected `Mapping[int | str, Any]`, found `(AbstractSet[int | str] & Top[Mapping[Unknown, object]]) | Mapping[int | str, Any] | dict[int | str, ellipsis]`

trio (https://github.com/python-trio/trio)
+ src/trio/_core/_tests/test_run.py:2440:49: warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- Found 469 diagnostics
+ Found 470 diagnostics

setuptools (https://github.com/pypa/setuptools)
- setuptools/_vendor/packaging/version.py:48:24: error[call-non-callable] Object of type `object` is not callable
- Found 1125 diagnostics
+ Found 1124 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 57 diagnostics
+ Found 58 diagnostics

egglog-python (https://github.com/egraphs-good/egglog-python)
- python/egglog/egraph.py:1962:24: error[invalid-argument-type] Argument to function `expr_action` is incorrect: Expected `BaseExpr`, found `(BaseExpr & ~Action) | (Fact & ~Action)`
- Found 1475 diagnostics
+ Found 1474 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
- hydpy/core/devicetools.py:2425:9: error[type-assertion-failure] Type `Literal["inlets", "outlets", "observers", "receivers", "senders", "inputs", "outputs"]` is not equivalent to `Never`
- Found 1081 diagnostics
+ Found 1080 diagnostics

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 16, 2026

Memory usage report

Summary

Project Old New Diff Outcome
flake8 47.96MB 48.02MB +0.13% (64.08kB)
trio 117.74MB 116.18MB -1.32% (1.56MB) ⬇️
sphinx 265.30MB 263.59MB -0.65% (1.72MB) ⬇️
prefect 704.16MB 697.56MB -0.94% (6.60MB) ⬇️

Significant changes

Click to expand detailed breakdown

flake8

Name Old New Diff Outcome
infer_definition_types 1.86MB 1.95MB +4.95% (94.31kB)
infer_expression_type_impl 157.40kB 213.58kB +35.69% (56.18kB)
semantic_index 13.83MB 13.78MB -0.34% (48.51kB)
infer_expression_types_impl 1.07MB 1.03MB -3.10% (33.84kB)
Expression 365.70kB 334.62kB -8.50% (31.08kB)
loop_header_reachability 13.70kB 43.39kB +216.83% (29.70kB)
CallableType 166.08kB 142.38kB -14.27% (23.70kB)
all_narrowing_constraints_for_expression 82.57kB 102.54kB +24.18% (19.96kB)
BoundMethodType<'db>::into_callable_type_ 26.96kB 13.43kB -50.20% (13.54kB)
infer_unpack_types 37.86kB 48.64kB +28.47% (10.78kB)
all_negative_narrowing_constraints_for_expression 40.25kB 45.98kB +14.23% (5.73kB)
infer_scope_types_impl 1003.61kB 1001.57kB -0.20% (2.04kB)
IntersectionType<'db>::from_two_elements_::interned_arguments 20.45kB 20.54kB +0.42% (88.00B)
IntersectionType<'db>::from_two_elements_ 19.30kB 19.35kB +0.28% (56.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 312.52kB 312.51kB -0.00% (12.00B)
... 1 more

trio

Name Old New Diff Outcome
infer_expression_types_impl 7.08MB 6.10MB -13.77% (997.62kB) ⬇️
Expression 1.41MB 1.09MB -22.28% (321.33kB) ⬇️
semantic_index 30.33MB 30.04MB -0.96% (298.10kB) ⬇️
infer_expression_type_impl 1.43MB 1.53MB +6.62% (97.03kB) ⬇️
CallableType 572.08kB 489.79kB -14.38% (82.29kB) ⬇️
all_narrowing_constraints_for_expression 601.43kB 640.09kB +6.43% (38.65kB) ⬇️
BoundMethodType<'db>::into_callable_type_ 75.16kB 43.76kB -41.78% (31.41kB) ⬇️
all_negative_narrowing_constraints_for_expression 184.88kB 213.03kB +15.23% (28.16kB) ⬇️
infer_scope_types_impl 4.79MB 4.77MB -0.48% (23.50kB) ⬇️
loop_header_reachability 134.84kB 139.52kB +3.46% (4.67kB) ⬇️
Type<'db>::member_lookup_with_policy_ 1.82MB 1.82MB +0.20% (3.67kB) ⬇️
infer_deferred_types 2.37MB 2.36MB -0.11% (2.73kB) ⬇️
StaticClassLiteral<'db>::implicit_attribute_inner_ 751.04kB 753.63kB +0.34% (2.59kB) ⬇️
is_redundant_with_impl 477.89kB 475.85kB -0.43% (2.04kB) ⬇️
Type<'db>::class_member_with_policy_ 2.00MB 2.00MB -0.10% (2.04kB) ⬇️
... 35 more

sphinx

Name Old New Diff Outcome
infer_expression_types_impl 21.59MB 19.93MB -7.68% (1.66MB) ⬇️
semantic_index 62.49MB 61.79MB -1.13% (720.29kB) ⬇️
Expression 3.17MB 2.50MB -21.11% (685.48kB) ⬇️
infer_expression_type_impl 3.22MB 3.86MB +19.73% (651.20kB) ⬇️
infer_definition_types 23.96MB 24.51MB +2.30% (563.57kB) ⬇️
CallableType 1.07MB 923.96kB -15.80% (173.39kB) ⬇️
all_narrowing_constraints_for_expression 2.35MB 2.49MB +6.26% (150.29kB) ⬇️
loop_header_reachability 383.40kB 525.22kB +36.99% (141.82kB) ⬇️
BoundMethodType<'db>::into_callable_type_ 278.39kB 175.89kB -36.82% (102.50kB) ⬇️
all_negative_narrowing_constraints_for_expression 1.01MB 1.09MB +7.62% (79.12kB) ⬇️
infer_scope_types_impl 15.59MB 15.57MB -0.12% (19.23kB) ⬇️
UnionType<'db>::from_two_elements_ 1.35MB 1.36MB +0.70% (9.64kB) ⬇️
infer_unpack_types 446.49kB 454.55kB +1.80% (8.05kB) ⬇️
is_redundant_with_impl::interned_arguments 2.07MB 2.08MB +0.33% (6.96kB) ⬇️
UnionType 1.23MB 1.24MB +0.42% (5.30kB) ⬇️
... 26 more

prefect

Name Old New Diff Outcome
infer_expression_types_impl 60.89MB 55.77MB -8.41% (5.12MB) ⬇️
semantic_index 173.44MB 171.55MB -1.09% (1.89MB) ⬇️
Expression 8.51MB 6.74MB -20.77% (1.77MB) ⬇️
infer_definition_types 88.65MB 89.84MB +1.34% (1.19MB) ⬇️
all_narrowing_constraints_for_expression 7.01MB 7.33MB +4.56% (327.29kB) ⬇️
infer_expression_type_impl 14.38MB 14.68MB +2.10% (308.86kB) ⬇️
CallableType 1.88MB 1.60MB -15.17% (292.17kB) ⬇️
StaticClassLiteral<'db>::implicit_attribute_inner_ 9.78MB 10.00MB +2.22% (221.96kB) ⬇️
all_negative_narrowing_constraints_for_expression 2.61MB 2.80MB +7.49% (199.93kB) ⬇️
Type<'db>::member_lookup_with_policy_ 15.89MB 16.08MB +1.18% (191.96kB) ⬇️
Type<'db>::class_member_with_policy_ 17.43MB 17.55MB +0.68% (121.40kB) ⬇️
BoundMethodType<'db>::into_callable_type_ 306.36kB 186.55kB -39.11% (119.81kB) ⬇️
loop_header_reachability 436.16kB 533.67kB +22.36% (97.51kB) ⬇️
infer_scope_types_impl 52.96MB 52.90MB -0.11% (60.48kB) ⬇️
Type<'db>::try_call_dunder_get_ 10.51MB 10.50MB -0.11% (11.53kB) ⬇️
... 28 more

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 16, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-await 0 40 0
call-non-callable 0 2 0
invalid-return-type 0 1 1
invalid-argument-type 0 1 0
type-assertion-failure 0 1 0
unused-type-ignore-comment 1 0 0
Total 1 45 1

Full report with detailed diff (timing results)

@sharkdp sharkdp marked this pull request as ready for review March 16, 2026 09:12
@sharkdp sharkdp merged commit a2c5af0 into main Mar 16, 2026
54 checks passed
@sharkdp sharkdp deleted the david/retry-remove-noreturn-opt branch March 16, 2026 09:12
carljm added a commit that referenced this pull request Mar 16, 2026
* main: (24 commits)
  Update astral-sh/setup-uv action to v7.6.0 (#24003)
  [ty] discover /usr/local/lib dist-packages on Debian/Ubuntu (#23797)
  [ty] Rename and invert logic of ReturnsNever constraints (#23997)
  [ty] Make ecosystem-analyzer the default workflow (#23996)
  [ty] Include CPython projects in ecosystem-analyzer runs (#23995)
  Update docker/setup-buildx-action action to v4 (#23992)
  Update actions/download-artifact digest to 484a0b5 (#23974)
  Update docker/metadata-action action to v6 (#23991)
  Update docker/login-action action to v4 (#23990)
  Update docker/build-push-action action to v7 (#23989)
  [ty] Completely remove the `NoReturn` shortcut optimization (episode 2) (#23994)
  Update dependency mkdocs-material to v9.7.4 (#23978)
  Update dependency ruff to v0.15.6 (#23979)
  Update dependency astral-sh/uv to v0.10.10 (#23977)
  Update cargo-bins/cargo-binstall action to v1.17.7 (#23975)
  Update CodSpeedHQ/action action to v4.11.1 (#23976)
  Update Rust crate jiff to v0.2.23 (#23982)
  Update Rust crate getrandom to v0.4.2 (#23981)
  Update actions/setup-node action to v6.3.0 (#23987)
  Update Rust crate toml to v1.0.6 (#23984)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer internal An internal refactor or improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants