-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Add new "constraint implication" typing relation #21010
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
Conversation
Diagnostic diff on typing conformance testsNo changes detected when running ty on typing conformance tests ✅ |
|
4c937b5 to
671582a
Compare
e3803a5 to
0827f40
Compare
9d07e4d to
86f2b08
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had originally written this as a new TypeRelation variant, but that was too heavy-handed, given that constraint implication is exactly the same as subtyping for everything except for typevars.
1b9e4f5 to
af813e7
Compare
ab271f0 to
3c7950c
Compare
| The interesting case is typevars. The other typing relationships (TODO: will) all "punt" on the | ||
| question when considering a typevar, by translating the desired relationship into a constraint set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This TODO is #20093
| static_assert(is_subtype_of_given(given_str, T, str)) | ||
| ``` | ||
|
|
||
| This might require propagating constraints from other typevars. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be involved enough that I'm tackling it separately, in #21068
| /// A type variable that has been bound to a generic context, and which can be specialized to a | ||
| /// concrete type. | ||
| #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] | ||
| #[derive(PartialOrd, Ord)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should always be comparing a typevar's identity, and this makes it harder to accidentally compare specific instances of a typevar.
| def is_subtype_of_given( | ||
| constraints: bool | ConstraintSet, ty: Any, of: Any | ||
| ) -> ConstraintSet: | ||
| """Returns a constraint set that is satisfied when `ty` is a `subtype`_ of `of`, | ||
| assuming that all of the constraints in `constraints` hold. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just wanted to suggest making this a method on ConstraintSet but that doesn't work because you also want to allow bool as constraints.
I'm not sure I fully understand the semantics here. Is this roughly if and only if constraints semantics? If that's the case, I think I'd find is_subtype_of_iff(ty, of, constraints) easier to understand.
But I don't think that's what this API does after reading the mdtests. Hmm :)
I'm asking because I first assumed the relation between is_subtype_of and is_subtype_of_given is similar to std::mem::size_of and std::mem::size_of_val where the latter takes a value. I don't think this is the case here because constraints would than have to be a subtype of ty, which the API doesn't require.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just wanted to suggest making this a method on
ConstraintSetbut that doesn't work because you also want to allowboolasconstraints.
Yeah, I thought the same thing, but our KnownFunction machinery is focused on top-level functions in a module. I had considered adding support for "known methods of a class", but that seemed like it would be out of scope for this PR. I can open an issue to track that.
I'm not sure I fully understand the semantics here. Is this roughly if and only if
constraintssemantics? If that's the case, I think I'd findis_subtype_of_iff(ty, of, constraints)easier to understand.
I can copy over more of the commentary from the mdtest to explain the method better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I put the expanded documentation on the Rust side, to be consistent with where we are documenting the other typing internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought the same thing, but our
KnownFunctionmachinery is focused on top-level functions in a module. I had considered adding support for "known methods of a class", but that seemed like it would be out of scope for this PR. I can open an issue to track that.
| static_assert(not is_subtype_of(bool, str)) | ||
| static_assert(not is_subtype_of_given(True, bool, str)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is is_subtype_of(T, V) guaranteed to always be the same as is_subtype_of(True, T, V). If so, maybe that's something you could add to the documentation of is_subtype_of_given
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great catch! I was in the middle of writing that this would not be true when comparing typevars — but you're right, and it is true even then:
is_subtype_of(bool, int) # yes
is_subtype_of_given(True, bool, int) # yes
is_subtype_of_given(False, bool, int) # yes
def f[T]():
is_subtype_of(T, int) # no (T could be anything!)
is_subtype_of_given(True, T, int) # also no (T could be anything!)This also made me realize that I don't need to (and shouldn't) project away the other typevars like I was doing.
| relation: TypeRelation, | ||
| ) -> Self { | ||
| let (lower, upper) = match relation { | ||
| // TODO: Is this the correct constraint for redundancy? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it? ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AlexWaygood do you have thoughts here? I was thinking that we should treat redundancy the same as subtyping here. Adding more context, the question is, if you call is_redundant_with(T, list[Any]), what constraint should we turn that into? For is_subtype_of(T, list[Any]), we turn it into T ≤ Bottom[list[Any]], since for subtyping, the relationship has to hold for all materializations. For is_assignable_to(T, list[Any]), we turn it into T ≤ Top[list[Any]], since the relationship only has to hold for some materialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to take a look at this, but might not get to it until Wednesday as I'm busy travelling tomorrow. Don't feel you have to wait for me before merging!
In general, if we're in doubt about how redundancy should behave, the safe thing to do in that situation is always to treat it like subtyping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This isn't hooked up to anything yet except isolated mdtests, so no urgency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(But I will still probably merge this and address anything you might find in a follow-on PR)
* origin/main: Respect `--output-format` with `--watch` (#21097) [`pyflakes`] Revert to stable behavior if imports for module lie in alternate branches for `F401` (#20878) Fix finding keyword range for clause header after statement ending with semicolon (#21067) [ty] Fix bug where ty would think all types had an `__mro__` attribute (#20995) Restore `indent.py` (#21094) [`flake8-django`] Apply `DJ001` to annotated fields (#20907) Clearer error message when `line-length` goes beyond threshold (#21072) Update upload and download artifacts github actions (#21083) Update dependency mdformat-mkdocs to v4.4.2 (#21088) Update cargo-bins/cargo-binstall action to v1.15.9 (#21086) Update Rust crate clap to v4.5.50 (#21090) Update Rust crate get-size2 to v0.7.1 (#21091) Update Rust crate bstr to v1.12.1 (#21089) Add missing docstring sections to the numpy list (#20931) [`pydoclint`] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) (#21011) [ty] Use constructor parameter types as type context (#21054)
…l-constraint-sets
* dcreager/refactor-constraint-mdtests: (60 commits)
add static_asserts
move all reveal diagnostics to separate line
add more gradual tests
better comment
restructure a bit
better names/comments
simplify before implication
two typevars!
mdformat
rename mdtest
move is_subtype_of_given into ConstraintSet
move where we grab these
add ConstraintSet.{always,never}
move range_constraint into ConstraintSet class
[ty] Rename `inner` query for better debugging experience (#21106)
[ty] Add new "constraint implication" typing relation (#21010)
[semantic error tests]: refactor semantic error tests to separate files (#20926)
Respect `--output-format` with `--watch` (#21097)
[`pyflakes`] Revert to stable behavior if imports for module lie in alternate branches for `F401` (#20878)
Fix finding keyword range for clause header after statement ending with semicolon (#21067)
...
…between two typevars (#21068) It's possible for a constraint to mention two typevars. For instance, in the body of ```py def f[S: int, T: S](): ... ``` the baseline constraint set would be `(T ≤ S) ∧ (S ≤ int)`. That is, `S` must specialize to some subtype of `int`, and `T` must specialize to a subtype of the type that `S` specializes to. This PR updates the new "constraint implication" relationship from #21010 to work on these kinds of constraint sets. For instance, in the example above, we should be able to see that `T ≤ int` must always hold: ```py def f[S, T](): constraints = ConstraintSet.range(Never, S, int) & ConstraintSet.range(Never, T, S) static_assert(constraints.implies_subtype_of(T, int)) # now succeeds! ``` This did not require major changes to the implementation of `implies_subtype_of`. That method already relies on how our `simplify` and `domain` methods expand a constraint set to include the transitive closure of the constraints that it mentions, and to mark certain combinations of constraints as impossible. Previously, that transitive closure logic only looked at pairs of constraints that constrain the same typevar. (For instance, to notice that `(T ≤ bool) ∧ ¬(T ≤ int)` is impossible.) Now we also look at pairs of constraints that constraint different typevars, if one of the constraints is bound by the other — that is, pairs of the form `T ≤ S` and `S ≤ something`, or `S ≤ T` and `something ≤ S`. In those cases, transitivity lets us add a new derived constraint that `T ≤ something` or `something ≤ T`, respectively. Having done that, our existing `implies_subtype_of` logic finds and takes into account that derived constraint.
This PR adds the new constraint implication relationship between types, aka
is_subtype_of_given, which tests whether one type is a subtype of another assuming that the constraints in a particular constraint set hold.For concrete types, constraint implication is exactly the same as subtyping. (A concrete type is any fully static type that is not a typevar. It can contain a typevar, though —
list[T]is considered concrete.)The interesting case is typevars. The other typing relationships (TODO: will) all "punt" on the question when considering a typevar, by translating the desired relationship into a constraint set. At some point, though, we need to resolve a constraint set; at that point, we can no longer punt on the question. Unlike with concrete types, the answer will depend on the constraint set that we are considering.