-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] WIP: Check typevar assignability using constraint sets #20093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dcreager
wants to merge
17
commits into
main
Choose a base branch
from
dcreager/real-constraint-sets
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Contributor
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-11-25 02:48:19.657726287 +0000
+++ new-output.txt 2025-11-25 02:48:23.676736894 +0000
@@ -1,4 +1,4 @@
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/17bc55d/src/function/execute.rs:469:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(1a6a3)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/17bc55d/src/function/execute.rs:469:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(1c6a3)): 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__`
@@ -545,7 +545,6 @@
generics_syntax_infer_variance.py:56:35: error[invalid-assignment] Object of type `ShouldBeCovariant3[int | float]` is not assignable to `ShouldBeCovariant3[int]`
generics_syntax_infer_variance.py:84:36: error[invalid-assignment] Object of type `ShouldBeCovariant5[int]` is not assignable to `ShouldBeCovariant5[int | float]`
generics_syntax_infer_variance.py:85:34: error[invalid-assignment] Object of type `ShouldBeCovariant5[int | float]` is not assignable to `ShouldBeCovariant5[int]`
-generics_syntax_infer_variance.py:92:9: error[invalid-assignment] Cannot assign to final attribute `x` on type `Self@__init__`
generics_syntax_infer_variance.py:95:36: error[invalid-assignment] Object of type `ShouldBeCovariant6[int]` is not assignable to `ShouldBeCovariant6[int | float]`
generics_syntax_infer_variance.py:96:34: error[invalid-assignment] Object of type `ShouldBeCovariant6[int | float]` is not assignable to `ShouldBeCovariant6[int]`
generics_syntax_infer_variance.py:112:38: error[invalid-assignment] Object of type `ShouldBeInvariant1[int]` is not assignable to `ShouldBeInvariant1[int | float]`
@@ -815,6 +814,7 @@
protocols_generic.py:66:25: error[invalid-assignment] Object of type `Sender[int]` is not assignable to `Sender[int | float]`
protocols_generic.py:74:28: error[invalid-assignment] Object of type `AttrProto[int]` is not assignable to `AttrProto[int | float]`
protocols_generic.py:75:26: error[invalid-assignment] Object of type `AttrProto[int | float]` is not assignable to `AttrProto[int]`
+protocols_generic.py:147:25: error[invalid-assignment] Object of type `ConcreteHasProperty4` is not assignable to `HasPropertyProto`
protocols_merging.py:52:25: error[invalid-assignment] Object of type `SCConcrete2` is not assignable to `SizedAndClosable1`
protocols_merging.py:53:25: error[invalid-assignment] Object of type `SCConcrete2` is not assignable to `SizedAndClosable2`
protocols_merging.py:54:25: error[invalid-assignment] Object of type `SCConcrete2` is not assignable to `SizedAndClosable3`
|
Contributor
|
7ddd258 to
3ae013c
Compare
d5c49ba to
c1441d2
Compare
CodSpeed Performance ReportMerging #20093 will degrade performances by 7.41%Comparing Summary
Benchmarks breakdown
Footnotes
|
This comment was marked as outdated.
This comment was marked as outdated.
c1441d2 to
c70e3bc
Compare
50f4f6e to
473413f
Compare
c70e3bc to
f06c0f6
Compare
d670aa9 to
4b4fc92
Compare
81cbc18 to
6514d3e
Compare
dcreager
added a commit
that referenced
this pull request
Aug 29, 2025
This PR adds an implementation of constraint sets.
An individual constraint restricts the specialization of a single
typevar to be within a particular lower and upper bound: the typevar can
only specialize to types that are a supertype of the lower bound, and a
subtype of the upper bound. (Note that lower and upper bounds are fully
static; we take the bottom and top materializations of the bounds to
remove any gradual forms if needed.) Either bound can be “closed” (where
the bound is a valid specialization), or “open” (where it is not).
You can then build up more complex constraint sets using union,
intersection, and negation operations. We use a disjunctive normal form
(DNF) representation, just like we do for types: a _constraint set_ is
the union of zero or more _clauses_, each of which is the intersection
of zero or more individual constraints. Note that the constraint set
that contains no clauses is never satisfiable (`⋃ {} = 0`); and the
constraint set that contains a single clause, which contains no
constraints, is always satisfiable (`⋃ {⋂ {}} = 1`).
One thing to note is that this PR does not change the logic of the
actual assignability checks, and in particular, we still aren't ever
trying to create an "individual constraint" that constrains a typevar.
Technically we're still operating only on `bool`s, since we only ever
instantiate `C::always_satisfiable` (i.e., `true`) and
`C::unsatisfiable` (i.e., `false`) in the `has_relation_to` methods. So
if you thought that #19838 introduced an unnecessarily complex stand-in
for `bool`, well here you go, this one is worse! (But still seemingly
not yielding a performance regression!) The next PR in this series,
#20093, is where we will actually create some non-trivial constraint
sets and use them in anger.
That said, the PR does go ahead and update the assignability checks to
use the new `ConstraintSet` type instead of `bool`. That part is fairly
straightforward since we had already updated the assignability checks to
use the `Constraints` trait; we just have to actively choose a different
impl type. (For the `is_whatever` variants, which still return a `bool`,
we have to convert the constraint set, but the explicit
`is_always_satisfiable` calls serve as nice documentation of our
intent.)
---------
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
6514d3e to
37b0ec9
Compare
Contributor
|
| Project | Old Status | New Status | Old Return Code | New Return Code |
|---|---|---|---|---|
koda-validate |
success | timeout | 1 |
None |
Diagnostic changes:
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
20 | 16 | 1 |
type-assertion-failure |
0 | 9 | 0 |
unused-ignore-comment |
8 | 0 | 0 |
invalid-return-type |
0 | 5 | 1 |
no-matching-overload |
1 | 3 | 0 |
possibly-unbound-attribute |
2 | 0 | 1 |
unsupported-operator |
0 | 3 | 0 |
non-subscriptable |
2 | 0 | 0 |
redundant-cast |
0 | 1 | 0 |
| Total | 33 | 37 | 3 |
474b036 to
ced99ce
Compare
second-ed
pushed a commit
to second-ed/ruff
that referenced
this pull request
Sep 9, 2025
This PR adds an implementation of constraint sets.
An individual constraint restricts the specialization of a single
typevar to be within a particular lower and upper bound: the typevar can
only specialize to types that are a supertype of the lower bound, and a
subtype of the upper bound. (Note that lower and upper bounds are fully
static; we take the bottom and top materializations of the bounds to
remove any gradual forms if needed.) Either bound can be “closed” (where
the bound is a valid specialization), or “open” (where it is not).
You can then build up more complex constraint sets using union,
intersection, and negation operations. We use a disjunctive normal form
(DNF) representation, just like we do for types: a _constraint set_ is
the union of zero or more _clauses_, each of which is the intersection
of zero or more individual constraints. Note that the constraint set
that contains no clauses is never satisfiable (`⋃ {} = 0`); and the
constraint set that contains a single clause, which contains no
constraints, is always satisfiable (`⋃ {⋂ {}} = 1`).
One thing to note is that this PR does not change the logic of the
actual assignability checks, and in particular, we still aren't ever
trying to create an "individual constraint" that constrains a typevar.
Technically we're still operating only on `bool`s, since we only ever
instantiate `C::always_satisfiable` (i.e., `true`) and
`C::unsatisfiable` (i.e., `false`) in the `has_relation_to` methods. So
if you thought that astral-sh#19838 introduced an unnecessarily complex stand-in
for `bool`, well here you go, this one is worse! (But still seemingly
not yielding a performance regression!) The next PR in this series,
astral-sh#20093, is where we will actually create some non-trivial constraint
sets and use them in anger.
That said, the PR does go ahead and update the assignability checks to
use the new `ConstraintSet` type instead of `bool`. That part is fairly
straightforward since we had already updated the assignability checks to
use the `Constraints` trait; we just have to actively choose a different
impl type. (For the `is_whatever` variants, which still return a `bool`,
we have to convert the constraint set, but the explicit
`is_always_satisfiable` calls serve as nice documentation of our
intent.)
---------
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
dcreager
added a commit
that referenced
this pull request
Oct 16, 2025
We have to track whether a typevar appears in a position where it's inferable or not. In a non-inferable position (in the body of the generic class or function that binds it), assignability must hold for every possible specialization of the typevar. In an inferable position, it only needs to hold for _some_ specialization. #20093 is working on using constraint sets to model assignability of typevars, and the constraint sets that we produce will be the same for inferable vs non-inferable typevars; what changes is what we _compare_ that constraint set to. (For a non-inferable typevar, the constraint set must equal the set of valid specializations; for an inferable typevar, it must not be `never`.) When I first added support for tracking inferable vs non-inferable typevars, it seemed like it would be easiest to have separate `Type` variants for each. The alternative (which lines up with the Δ set in [POPL15](https://doi.org/10.1145/2676726.2676991)) would be to explicitly plumb through a list of inferable typevars through our type property methods. That seemed cumbersome. In retrospect, that was the wrong decision. We've had to jump through hoops to translate types between the inferable and non-inferable variants, which has been quite brittle. Combined with the original point above, that much of the assignability logic will become more identical between inferable and non-inferable, there is less justification for the two `Type` variants. And plumbing an extra `inferable` parameter through all of these methods turns out to not be as bad as I anticipated. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This was referenced Oct 24, 2025
dcreager
added a commit
that referenced
this pull request
Oct 28, 2025
This PR updates the mdtests that test how our generics solver interacts with our new constraint set implementation. Because the rendering of a constraint set can get long, this standardizes on putting the `revealed` assertion on a separate line. We also add a `static_assert` test for each constraint set to verify that they are all coerced into simple `bool`s correctly. This is a pure reformatting (not even a refactoring!) that changes no behavior. I've pulled it out of #20093 to reduce the amount of effort that will be required to review that PR.
dcreager
added a commit
that referenced
this pull request
Nov 5, 2025
…ection) (#21273) This PR carries over some of the `has_relation_to` logic for comparing a typevar with itself. A typevar will specialize to the same type if it's mentioned multiple times, so it is always assignable to and a subtype of itself. (Note that typevars can only specialize to fully static types.) This is also true when the typevar appears in a union on the right-hand side, or in an intersection on the left-hand side. Similarly, a typevar is always disjoint from its negation, so when a negated typevar appears on the left-hand side, the constraint set is never satisfiable. (Eventually this will allow us to remove the corresponding clauses from `has_relation_to`, but that can't happen until more of #20093 lands.)
dcreager
added a commit
that referenced
this pull request
Nov 17, 2025
Constraint sets can now track subtyping/assignability/etc of generic
callables correctly. For instance:
```py
def identity[T](t: T) -> T:
return t
constraints = ConstraintSet.always()
static_assert(constraints.implies_subtype_of(TypeOf[identity], Callable[[int], int]))
static_assert(constraints.implies_subtype_of(TypeOf[identity], Callable[[str], str]))
```
A generic callable can be considered an intersection of all of its
possible specializations, and an assignability check with an
intersection as the lhs side succeeds of _any_ of the intersected types
satisfies the check. Put another way, if someone expects to receive any
function with a signature of `(int) -> int`, we can give them
`identity`.
Note that the corresponding check using `is_subtype_of` directly does
not yet work, since #20093 has not yet hooked up the core typing
relationship logic to use constraint sets:
```py
# These currently fail
static_assert(is_subtype_of(TypeOf[identity], Callable[[int], int]))
static_assert(is_subtype_of(TypeOf[identity], Callable[[str], str]))
```
To do this, we add a new _existential quantification_ operation on
constraint sets. This takes in a list of typevars and _removes_ those
typevars from the constraint set. Conceptually, we return a new
constraint set that evaluates to `true` when there was _any_ assignment
of the removed typevars that caused the old constraint set to evaluate
to `true`.
When comparing a generic constraint set, we add its typevars to the
`inferable` set, and figure out whatever constraints would allow any
specialization to satisfy the check. We then use the new existential
quantification operator to remove those new typevars, since the caller
doesn't (and shouldn't) know anything about them.
---------
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
519a9c3 to
8e754a4
Compare
8e754a4 to
3bc3ced
Compare
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This stacks on top of #19997, and uses the new constraint set implementation to check assignability for typevars.
Not ready for review yet.