Skip to content

Conversation

carljm
Copy link
Contributor

@carljm carljm commented Sep 27, 2025

Summary

This allows us to handle self-referential bounds/constraints/defaults without panicking.

Handles more cases from astral-sh/ty#256

This also changes the way we infer the types of legacy TypeVars. Rather than understanding a constructor call to typing[_extension].TypeVar inside of any (arbitrarily nested) expression, and having to use a special assigned_to field of the semantic index to try to best-effort figure out what name the typevar was assigned to, we instead understand the creation of a legacy TypeVar only in the supported syntactic position (RHS of a simple un-annotated assignment with one target). In any other position, we just infer it as creating an opaque instance of typing.TypeVar. (This behavior matches all other type checkers.)

So we now special-case TypeVar creation in TypeInferenceBuilder, as a special case of an assignment definition, rather than deeper inside call binding. This does mean we re-implement slightly more of argument-parsing, but in practice this is minimal and easy to handle correctly.

This is easier to implement if we also make the RHS of a simple (no unpacking) one-target assignment statement no longer a standalone expression. Which is fine to do, because simple one-target assignments don't need to infer the RHS more than once. This is a bonus performance (0-3% across various projects) and significant memory-usage win, since most assignment statements are simple one-target assignment statements, meaning we now create many fewer standalone-expression salsa ingredients.

This change does mean that inference of manually-constructed TypeAliasType instances can no longer find its Definition in assigned_to, which regresses go-to-definition for these aliases. In a future PR, TypeAliasType will receive the same treatment that TypeVar did in this PR (moving its special-case inference into TypeInferenceBuilder and supporting it only in the correct syntactic position, and lazily inferring its value type to support recursion), which will also fix the go-to-definition regression. (I decided a temporary edge-case regression is better in this case than doubling the size of this PR.)

This PR also tightens up and fixes various aspects of the validation of TypeVar creation, as seen in the tests.

We still (for now) treat all typevars as instances of typing.TypeVar, even if they were created using typing_extensions.TypeVar. This means we'll wrongly error on e.g. T.__default__ on Python 3.11, even if T is a typing_extensions.TypeVar instance at runtime. We share this wrong behavior with both mypy and pyrefly. It will be easier to fix after we pull in python/typeshed#14840.

There are some issues that showed up here with typevar identity and MarkTypeVarsInferable; the fix here (using the new original field and is_identical_to methods on BoundTypeVarInstance and TypeVarInstance) is a bit kludgy, but it can go away when we eliminate MarkTypeVarsInferable.

Test Plan

Added and updated mdtests.

Conformance suite impact

The impact here is all positive:

  • We now correctly error on a legacy TypeVar with exactly one constraint type given.
  • We now correctly error on a legacy TypeVar with both an upper bound and constraints specified.

Ecosystem impact

Basically none; in the setuptools case we just issue slightly different errors on an invalid TypeVar definition, due to the modified validation code.

@carljm carljm added the ty Multi-file analysis & type inference label Sep 27, 2025
Copy link
Contributor

github-actions bot commented Sep 27, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-10-09 21:06:16.051150791 +0000
+++ new-output.txt	2025-10-09 21:06:19.328159431 +0000
@@ -1,6 +1,6 @@
 WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
 fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/29ab321/src/function/execute.rs:217:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_type_statement.py`: `PEP695TypeAliasType < 'db >::value_type_(Id(cc17)): execute: too many cycle iterations`
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/29ab321/src/function/execute.rs:217:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(1643f)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/29ab321/src/function/execute.rs:217:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(1603f)): execute: too many cycle iterations`
 _directives_deprecated_library.py:15:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
 _directives_deprecated_library.py:30:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
 _directives_deprecated_library.py:36:41: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__add__`
@@ -364,6 +364,7 @@
 generics_base_class.py:49:22: error[too-many-positional-arguments] Too many positional arguments to class `LinkedList`: expected 1, got 2
 generics_base_class.py:61:18: error[too-many-positional-arguments] Too many positional arguments to class `MyDict`: expected 1, got 2
 generics_basic.py:34:12: error[unsupported-operator] Operator `+` is unsupported between objects of type `AnyStr@concat` and `AnyStr@concat`
+generics_basic.py:49:44: error[invalid-legacy-type-variable] A `TypeVar` cannot have exactly one constraint
 generics_basic.py:139:5: error[type-assertion-failure] Argument does not have asserted type `int`
 generics_basic.py:140:5: error[type-assertion-failure] Argument does not have asserted type `int`
 generics_basic.py:157:5: error[invalid-argument-type] Method `__getitem__` of type `bound method MyMap1[str, int].__getitem__(key: str, /) -> int` cannot be called with key of type `Literal[0]` on object of type `MyMap1[str, int]`
@@ -575,7 +576,8 @@
 generics_upper_bound.py:43:1: error[type-assertion-failure] Argument does not have asserted type `list[int] | set[int]`
 generics_upper_bound.py:51:8: error[invalid-argument-type] Argument to function `longer` is incorrect: Argument type `Literal[3]` does not satisfy upper bound `Sized` of type variable `ST`
 generics_upper_bound.py:51:11: error[invalid-argument-type] Argument to function `longer` is incorrect: Argument type `Literal[3]` does not satisfy upper bound `Sized` of type variable `ST`
-generics_variance.py:14:6: error[invalid-legacy-type-variable] A legacy `typing.TypeVar` cannot be both covariant and contravariant
+generics_upper_bound.py:56:10: error[invalid-legacy-type-variable] A `TypeVar` cannot have both a bound and constraints
+generics_variance.py:14:6: error[invalid-legacy-type-variable] A `TypeVar` cannot be both covariant and contravariant
 generics_variance.py:26:27: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Iterator[T_co@ImmutableList]`
 generics_variance.py:57:28: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `B_co@func`
 generics_variance.py:175:25: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
@@ -898,5 +900,5 @@
 typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
 typeddicts_usage.py:28:18: error[invalid-key] Invalid key access on TypedDict `Movie`: Unknown key "title"
 typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 899 diagnostics
+Found 901 diagnostics
 WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.

Copy link
Contributor

github-actions bot commented Sep 27, 2025

mypy_primer results

Changes were detected when running on open source projects
spack (https://github.com/spack/spack)
- lib/spack/spack/vendor/typing_extensions.py:313:12: error[invalid-legacy-type-variable] A legacy `typing.TypeVar` must be immediately assigned to a variable
+ lib/spack/spack/vendor/typing_extensions.py:313:12: error[invalid-legacy-type-variable] A `TypeVar` definition must be a simple variable assignment

setuptools (https://github.com/pypa/setuptools)
- setuptools/_vendor/typing_extensions.py:256:12: error[invalid-legacy-type-variable] A legacy `typing.TypeVar` must be immediately assigned to a variable
+ setuptools/_vendor/typing_extensions.py:256:12: error[invalid-legacy-type-variable] A `TypeVar` definition must be a simple variable assignment
- setuptools/_vendor/typing_extensions.py:1517:27: error[invalid-legacy-type-variable] The `contravariant` parameter of a legacy `typing.TypeVar` cannot have an ambiguous value
- setuptools/_vendor/typing_extensions.py:1517:68: error[invalid-type-form] Variable of type `None` is not allowed in a type expression
+ setuptools/_vendor/typing_extensions.py:1517:48: error[invalid-legacy-type-variable] Starred arguments are not supported in `TypeVar` creation
- Found 800 diagnostics
+ Found 799 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/_wheelfile.py:51:22: error[no-matching-overload] No overload of function `field` matches arguments
- Found 52 diagnostics
+ Found 51 diagnostics
Memory usage changes were detected when running on open source projects
trio (https://github.com/python-trio/trio)
-     memo fields = ~108MB
+     memo fields = ~103MB

sphinx (https://github.com/sphinx-doc/sphinx)
-     memo metadata = ~42MB
+     memo metadata = ~40MB

prefect (https://github.com/PrefectHQ/prefect)
- TOTAL MEMORY USAGE: ~541MB
+ TOTAL MEMORY USAGE: ~515MB
-     struct metadata = ~30MB
+     struct metadata = ~28MB
-     struct fields = ~35MB
+     struct fields = ~33MB
-     memo fields = ~384MB
+     memo fields = ~366MB

@carljm carljm force-pushed the cjm/legacytv branch 2 times, most recently from b7662e9 to 46a2e8b Compare September 27, 2025 02:14

This comment was marked as outdated.

Copy link

codspeed-hq bot commented Sep 27, 2025

CodSpeed Performance Report

Merging #20598 will improve performances by 5.56%

Comparing cjm/legacytv (75eda5e) with main (b086ffe)

Summary

⚡ 1 improvement
✅ 20 untouched
⏩ 30 skipped1

Benchmarks breakdown

Mode Benchmark BASE HEAD Change
WallTime small[freqtrade] 5.2 s 4.9 s +5.56%

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@carljm carljm force-pushed the cjm/legacytv branch 2 times, most recently from 9f019f3 to 125dff3 Compare October 6, 2025 20:22
carljm added a commit that referenced this pull request Oct 8, 2025
## Summary

Typevar attributes (bound/constraints/default) can be either lazily
evaluated or eagerly evaluated. Currently they are lazily evaluated for
PEP 695 typevars, and eager for legacy and synthetic typevars.
#20598 will make them lazy also
for legacy typevars, and the ecosystem report on that PR surfaced the
issue fixed here (because legacy typevars are much more common in the
ecosystem than PEP 695 typevars.)

Applying a transform to a typevar (normalization, materialization, or
mark-inferable) will reify all lazy attributes and create a new typevar
with eager attributes. In terms of Salsa identity, this transformed
typevar will be considered different from the original typevar, whether
or not the attributes were actually transformed.

In general, this is not a problem, since all typevars in a given generic
context will be transformed, or not, together.

The exception to this was implicit-self vs explicit Self annotations.
The typevar we created for implicit self was created initially using
inferable typevars, whereas an explicit Self annotation is initially
non-inferable, then transformed via mark-inferable when accessed as part
of a function signature. If the containing class (which becomes the
upper bound of `Self`) is generic, and has e.g. a lazily-evaluated
default, then the explicit-Self annotation will reify that default in
the upper bound, and the implicit-self would not, leading them to be
treated as different typevars, and causing us to fail to solve a call to
a method such as `def method(self) -> Self` correctly.

The fix here is to treat implicit-self more like explicit-Self,
initially creating it as non-inferable and then using the mark-inferable
transform on it. This is less efficient, but restores the invariant that
all typevars in a given generic context are transformed together, or
not, fixing the bug.

In the improved-constraint-solver work, the separation of typevars into
"inferable" and "non-inferable" is expected to disappear, along with the
mark-inferable transform, which would render both this bug and the fix
moot. So this fix is really just temporary until that lands.

There is a performance regression, but not a huge one: 1-2% on most
projects, 5% on one outlier. This seems acceptable, given that it should
be fully recovered by removing the mark-inferable transform.

## Test Plan

Added mdtests that failed before this change.
@carljm carljm marked this pull request as ready for review October 9, 2025 00:02
| ^^^^^
|
"#);
// TODO: This should jump to the definition of `Alias` above.
Copy link
Member

Choose a reason for hiding this comment

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

Can we create an issue for this. It's otherwise hard to remember

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it will be hard to remember, because I have to fix it in order to fix the cases in astral-sh/ty#256 related to TypeAliasType. So in that sense we already have an issue that ensures we won't forget it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(And I plan to do this PR next.)

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Only reviewed the tests so far

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

🚀

@carljm carljm enabled auto-merge (squash) October 9, 2025 21:07
@carljm carljm merged commit 8248193 into main Oct 9, 2025
39 checks passed
@carljm carljm deleted the cjm/legacytv branch October 9, 2025 21:08
@carljm
Copy link
Contributor Author

carljm commented Oct 9, 2025

Thanks @AlexWaygood for the excellent review here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants