Skip to content

[ty] Unify polarity-aware relation construction - #27707

Merged
carljm merged 8 commits into
mainfrom
cjm/3557-polarity-aware-relations
Aug 13, 2026
Merged

[ty] Unify polarity-aware relation construction#27707
carljm merged 8 commits into
mainfrom
cjm/3557-polarity-aware-relations

Conversation

@carljm

@carljm carljm commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of astral-sh/ty#3557 and the second step in rebooting #26712 as smaller focused changes.

On main, generic inference does not consistently propagate the variance of an enclosing generic class into structural-protocol and callable comparisons. Those comparisons instead construct the forward actual <= formal relation even when the surrounding position is contravariant or invariant. This produces incorrect bounds and inferred types:

from typing import Callable

class Middle: ...
class Derived(Middle): ...

class Sink[T]:
    def put(self, value: T) -> None: ...

def infer[T](sink: Sink[Callable[[], T]], value: T) -> T:
    return value

reveal_type(infer(Sink[Callable[[], Middle]](), Derived()))
# main: Middle
# this PR: Derived

This PR centralizes relation construction so inference consistently follows the surrounding polarity: covariance requires actual <= formal, contravariance requires formal <= actual, invariance requires both, and bivariance adds no constraints. The fix applies to nested generic protocols, callable signatures, callback protocols, TypedDict/protocol relations, and materialized protocols while preserving existing nominal inference, callable-union alternatives, TypedDict-union optimizations, and cycle-safe handling of recursive protocols.

Limitation

The new constraint solver does not yet support ParamSpec or TypeVarTuple. Whenever an inference context includes either variadic, all affected relations must continue through the old, forward-only inference path, including comparisons involving ordinary type variables alongside an unrelated variadic. This deliberately avoids regressions in parameter inference, structural protocols, descriptor signatures, and downstream argument diagnostics, but it also means the polarity fixes in this PR do not apply whenever variadics are involved. Focused TODO tests document cases that still incorrectly infer Middle instead of Derived; fixing those cases requires adding variadic support to the new solver.

Test plan

Added mdtests cover nested nominal classes, generic protocols, callable signatures, and callback protocols under covariance, contravariance, and invariance; double contravariance, callable-union alternatives, and invariant rejection; finite, recursive, and recursive-only materialized protocols; invariant, contravariant, and covariant ParamSpec inference through Concatenate; bounded and constrained prefixes, descriptor receivers, structural protocol unions, nominal callable objects, higher-order callbacks, and mixed ordinary/variadic inference; analogous TypeVarTuple callable, protocol, structural-union, and nominal-implementation cases; and explicit TODO coverage for unrelated variadics suppressing otherwise-correct polarity.

Additional mdtests cover repeated comparisons of the same protocol or callable under opposite polarities, plus overloaded callable acceptance and rejection across covariant, contravariant, and invariant wrappers.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Aug 13, 2026
@astral-sh-bot

astral-sh-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 96.98%. The percentage of expected errors that received a diagnostic held steady at 93.62%. The number of fully passing files held steady at 106/133.

@astral-sh-bot

astral-sh-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 449.30MB 449.35MB +0.01% (48.41kB)
sphinx 164.79MB 164.81MB +0.01% (20.19kB)
trio 92.26MB 92.27MB +0.00% (1.75kB)
flake8 39.69MB 39.70MB +0.00% (688.00B)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
when_constraint_set_assignable_to_owned_impl 5.54MB 5.58MB +0.76% (43.34kB)
TypePair 7.73MB 7.74MB +0.14% (11.34kB)
infer_definition_types 34.48MB 34.48MB -0.01% (3.59kB) ⬇️
infer_expression_types_impl 23.28MB 23.27MB -0.01% (2.20kB) ⬇️
infer_statement_types_impl 651.35kB 650.41kB -0.14% (960.00B) ⬇️
all_narrowing_constraints_for_expression 4.51MB 4.51MB +0.01% (576.00B)
member_lookup_with_policy_inner 8.91MB 8.91MB -0.00% (232.00B) ⬇️
infer_scope_types_impl 18.78MB 18.78MB +0.00% (216.00B)
infer_unpack_types 429.18kB 429.36kB +0.04% (184.00B)
member_lookup_with_policy_and_receiver_inner 1.96MB 1.96MB -0.01% (184.00B) ⬇️
CallableType 8.00MB 8.00MB +0.00% (144.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 796.62kB 796.49kB -0.02% (136.00B) ⬇️
infer_expression_type_impl 235.00kB 234.88kB -0.05% (128.00B) ⬇️
loop_header_reachability 261.58kB 261.68kB +0.04% (104.00B)
analyze_non_terminal_call 1023.48kB 1023.45kB -0.00% (40.00B) ⬇️
... 2 more

sphinx

Name Old New Diff Outcome
when_constraint_set_assignable_to_owned_impl 1.46MB 1.47MB +1.20% (17.82kB)
TypePair 2.12MB 2.12MB +0.25% (5.44kB)
infer_expression_types_impl 11.52MB 11.52MB -0.01% (1.51kB) ⬇️
infer_definition_types 10.95MB 10.95MB -0.01% (912.00B) ⬇️
infer_statement_types_impl 503.20kB 502.94kB -0.05% (264.00B) ⬇️
analyze_non_terminal_call 478.10kB 477.96kB -0.03% (144.00B) ⬇️
all_narrowing_constraints_for_expression 2.25MB 2.25MB -0.01% (144.00B) ⬇️
infer_scope_types_impl 6.08MB 6.08MB -0.00% (120.00B) ⬇️
infer_deferred_types 2.11MB 2.11MB -0.00% (8.00B) ⬇️

trio

Name Old New Diff Outcome
when_constraint_set_assignable_to_owned_impl 1003.16kB 1007.66kB +0.45% (4.49kB)
TypePair 963.00kB 964.22kB +0.13% (1.22kB)
infer_expression_types_impl 5.21MB 5.21MB -0.02% (1.17kB) ⬇️
infer_definition_types 4.43MB 4.43MB -0.03% (1.15kB) ⬇️
analyze_non_terminal_call 414.98kB 414.66kB -0.08% (320.00B) ⬇️
infer_expression_type_impl 41.73kB 41.41kB -0.75% (320.00B) ⬇️
all_narrowing_constraints_for_expression 639.20kB 638.98kB -0.03% (224.00B) ⬇️
loop_header_reachability 79.32kB 79.12kB -0.26% (208.00B) ⬇️
StaticClassLiteral<'db>::implicit_attribute_inner_ 103.91kB 103.71kB -0.20% (208.00B) ⬇️
member_lookup_with_policy_and_receiver_inner 234.48kB 234.32kB -0.07% (160.00B) ⬇️
member_lookup_with_policy_inner 1.22MB 1.22MB -0.01% (160.00B) ⬇️
infer_scope_types_impl 2.94MB 2.94MB -0.00% (80.00B) ⬇️

flake8

Name Old New Diff Outcome
when_constraint_set_assignable_to_owned_impl 160.76kB 160.95kB +0.12% (192.00B)
TypePair 230.81kB 231.00kB +0.08% (192.00B)
infer_definition_types 1.06MB 1.06MB +0.01% (112.00B)
infer_expression_types_impl 762.71kB 762.81kB +0.01% (96.00B)
all_narrowing_constraints_for_expression 107.05kB 107.09kB +0.04% (48.00B)
loop_header_reachability 9.70kB 9.73kB +0.32% (32.00B)
analyze_non_terminal_call 34.20kB 34.21kB +0.05% (16.00B)

@astral-sh-bot

astral-sh-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Full report with detailed diff (timing results)

@carljm
carljm marked this pull request as ready for review August 13, 2026 01:17
@carljm
carljm requested a review from a team as a code owner August 13, 2026 01:17
@astral-sh-bot
astral-sh-bot Bot requested a review from charliermarsh August 13, 2026 01:17
Comment thread crates/ty_python_semantic/src/types/generics.rs Outdated
@charliermarsh

Copy link
Copy Markdown
Member

The change makes sense to me conceptually though not super clear to me from the summary how much we're changing behavior here vs. a pure refactor. (My read is that this is fixing some limitations, it's not a pure refactor.)

@carljm
carljm marked this pull request as draft August 13, 2026 01:56
@carljm

carljm commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Yes, this is not a refactor, it's fixing the fact that we were incorrectly always treating several kinds of relations in SpecializationBuilder as if they were covariant relations (where the actual argument must be assignable to the formal parameter), and ignoring the actual polarity. So e.g. for protocols nested inside Callable arguments or whatever, the direction should be flipped and we weren't doing that. This PR fixes that. I'll make the description clearer, thanks.

@carljm
carljm marked this pull request as ready for review August 13, 2026 05:23
@astral-sh-bot
astral-sh-bot Bot requested a review from dhruvmanila August 13, 2026 05:23
@carljm
carljm merged commit 59196ba into main Aug 13, 2026
65 checks passed
@carljm
carljm deleted the cjm/3557-polarity-aware-relations branch August 13, 2026 05:26
George-Ogden pushed a commit to George-Ogden/ruff that referenced this pull request Aug 16, 2026
## Summary

Part of astral-sh/ty#3557 and the second step in rebooting astral-sh#26712 as
smaller focused changes.

On `main`, generic inference does not consistently propagate the
variance of an enclosing generic class into structural-protocol and
callable comparisons. Those comparisons instead construct the forward
`actual <= formal` relation even when the surrounding position is
contravariant or invariant. This produces incorrect bounds and inferred
types:

```py
from typing import Callable

class Middle: ...
class Derived(Middle): ...

class Sink[T]:
    def put(self, value: T) -> None: ...

def infer[T](sink: Sink[Callable[[], T]], value: T) -> T:
    return value

reveal_type(infer(Sink[Callable[[], Middle]](), Derived()))
# main: Middle
# this PR: Derived
```

This PR centralizes relation construction so inference consistently
follows the surrounding polarity: covariance requires `actual <=
formal`, contravariance requires `formal <= actual`, invariance requires
both, and bivariance adds no constraints. The fix applies to nested
generic protocols, callable signatures, callback protocols,
TypedDict/protocol relations, and materialized protocols while
preserving existing nominal inference, callable-union alternatives,
TypedDict-union optimizations, and cycle-safe handling of recursive
protocols.

## Limitation

The new constraint solver does not yet support `ParamSpec` or
`TypeVarTuple`. Whenever an inference context includes either variadic,
all affected relations must continue through the old, forward-only
inference path, including comparisons involving ordinary type variables
alongside an unrelated variadic. This deliberately avoids regressions in
parameter inference, structural protocols, descriptor signatures, and
downstream argument diagnostics, but it also means the polarity fixes in
this PR do not apply whenever variadics are involved. Focused TODO tests
document cases that still incorrectly infer `Middle` instead of
`Derived`; fixing those cases requires adding variadic support to the
new solver.

## Test plan

Added mdtests cover nested nominal classes, generic protocols, callable
signatures, and callback protocols under covariance, contravariance, and
invariance; double contravariance, callable-union alternatives, and
invariant rejection; finite, recursive, and recursive-only materialized
protocols; invariant, contravariant, and covariant `ParamSpec` inference
through `Concatenate`; bounded and constrained prefixes, descriptor
receivers, structural protocol unions, nominal callable objects,
higher-order callbacks, and mixed ordinary/variadic inference; analogous
`TypeVarTuple` callable, protocol, structural-union, and
nominal-implementation cases; and explicit TODO coverage for unrelated
variadics suppressing otherwise-correct polarity.

Additional mdtests cover repeated comparisons of the same protocol or
callable under opposite polarities, plus overloaded callable acceptance
and rejection across covariant, contravariant, and invariant wrappers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants