-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Heterogeneous unpacking support for unions #20377
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 ✅ |
|
9c25d55 to
d3ac1ac
Compare
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
0 | 6 | 0 |
parameter-already-assigned |
0 | 1 | 0 |
| Total | 0 | 7 | 0 |
CodSpeed Performance ReportMerging #20377 will improve performances by 6.26%Comparing Summary
Benchmarks breakdown
Footnotes
|
| fn non_async_special_case<'db>( | ||
| db: &'db dyn Db, | ||
| ty: Type<'db>, | ||
| ) -> Option<Cow<'db, TupleSpec<'db>>> { |
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.
other than the Type::Union branch, this is exactly the same code that used to exist in the match statement slightly lower down. It's just been extracted into a standalone function (so that it can be called recursively), and moved higher up to satisfy Clippy.
2fe357a to
a377c0a
Compare
|
We've supported unpacking of unions like this in assignments, but it looks like this implementation might be more general. Can we replace the older union-related code in |
That's a great question. I've just spent a while looking at this (probably longer than I should have!), and my answer is... I don't think so, unfortunately. Obtaining a different tuple spec for each union member, like I experimented with having struct IterationOutcome<'db>(smallvec::SmallVec<[Cow<'db, TupleSpec<'db>>; 1]>);but even that is too lossy a representation for struct IterationOutcome<'db>(
smallvec::SmallVec<[Result<Cow<'db, TupleSpec<'db>>, IterationError<'db>>; 1]>
);At which point the abstraction becomes so complicated, that I think it's probably not worth it anymore. So the question is: what does this PR actually get us? Firstly it gets us much better call-binding for calls with variadic parameters: unlike in ruff/crates/ty_python_semantic/src/types/call/bind.rs Lines 2283 to 2291 in ac2c530
Secondly, it gets us much more precise inference even if all you want is the homogeneous element type. For example: from typing import Literal
def f(x: Literal["abc", "def"]):
for item in x:
# main: LiteralString
# This PR: Literal["a", "b", "c", "d", "e", "f"]
reveal_type(item) |
dcreager
left a comment
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.
That's a great question. I've just spent a while looking at this (probably longer than I should have!), and my answer is... I don't think so, unfortunately.
Thanks for looking at that! No argument with your findings. Can you summarize them in the code? Possibly in unpacker.rs, to explain why you can't use the union-handling logic that we use for argument splatting
| let return_type = if let Type::Union(union) = argument { | ||
| union.map(db, |element| { | ||
| Type::tuple(TupleType::new(db, &element.iterate(db))) | ||
| }) | ||
| } else { | ||
| Type::tuple(TupleType::new(db, &argument.iterate(db))) | ||
| }; | ||
| overload.set_return_type(return_type); |
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.
Not blocking for this PR, but this pattern seems to come up enough to deserve a helper method on Type — a map_over_union that applies the function to each union element if it's a union, or to the individual type if not.
| } | ||
| } | ||
|
|
||
| fn all_elements(&self) -> impl Iterator<Item = &Type<'db>> { |
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.
nit: Can you throw a copied onto each iterator so that this can return Iterator<Item = Type>? I find it good karma to not make the caller have to worry about that
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.
Hmm, I feel like here it's more ergonomic to have it return Iterator<Item = &Type<'db>>, because then it matches the signature of TupleSpec::all_elements (and, indeed, the two other all_elements() methods in this module!):
ruff/crates/ty_python_semantic/src/types/tuple.rs
Lines 343 to 345 in 6f468ae
| pub(crate) fn all_elements(&self) -> impl Iterator<Item = &T> { | |
| self.0.iter() | |
| } |
it seems like strictly more code for not much gain here, if I make it return Iterator<Item = Type<'db>> rather than Iterator<Item = &Type<'db>>?
diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs
index dcb3df675d..cc39e696f8 100644
--- a/crates/ty_python_semantic/src/types/tuple.rs
+++ b/crates/ty_python_semantic/src/types/tuple.rs
@@ -1583,7 +1583,7 @@ impl<'db> TupleSpecBuilder<'db> {
}
}
- fn all_elements(&self) -> impl Iterator<Item = &Type<'db>> {
+ fn all_elements(&self) -> impl Iterator<Item = Type<'db>> {
match self {
TupleSpecBuilder::Fixed(elements) => Either::Left(elements.iter()),
TupleSpecBuilder::Variable {
@@ -1592,6 +1592,7 @@ impl<'db> TupleSpecBuilder<'db> {
suffix,
} => Either::Right(prefix.iter().chain(std::iter::once(variable)).chain(suffix)),
}
+ .copied()
}
/// Return a new tuple-spec builder that reflects the union of this tuple and another tuple.
@@ -1625,8 +1626,10 @@ impl<'db> TupleSpecBuilder<'db> {
// would actually lead to more precise inference, so it's probably not worth the
// complexity.
_ => {
- let unioned =
- UnionType::from_elements(db, self.all_elements().chain(other.all_elements()));
+ let unioned = UnionType::from_elements(
+ db,
+ self.all_elements().chain(other.all_elements().copied()),
+ );
TupleSpecBuilder::Variable {
prefix: vec![],
variable: unioned,6f468ae to
8dde577
Compare
…rable * origin/main: Don't use codspeed or depot runners in CI jobs on forks (#20894) [ty] cache Type::is_redundant_with (#20477) Fix run-away for mutually referential instance attributes (#20645) [ty] Limit shown import paths to at most 5 unless ty runs with `-v` (#20912) [ty] Use field-specifier return type as the default type for the field (#20915) [ty] Do not assume that `field`s have a default value (#20914) [ty] Fix match pattern value narrowing to use equality semantics (#20882) Update setup instructions for Zed 0.208.0+ (#20902) Move TOML indent size config (#20905) [syntax-errors]: implement F702 as semantic syntax error (#20869) [ty] Heterogeneous unpacking support for unions (#20377) [ty] refactor `Place` (#20871) Auto-accept snapshot changes as part of typeshed-sync PRs (#20892) [`airflow`] Add warning to `airflow.datasets.DatasetEvent` usage (`AIR301`) (#20551) [`flake8-pyi`] Fix operator precedence by adding parentheses when needed (`PYI061`) (#20508) [`pyupgrade`] Fix false negative for `TypeVar` with default argument in `non-pep695-generic-class` (`UP046`) (#20660) Update parser snapshots (#20893) Fix syntax error false positives for escapes and quotes in f-strings (#20867)
…nt-sets * dcreager/non-non-inferable: Don't use codspeed or depot runners in CI jobs on forks (#20894) [ty] cache Type::is_redundant_with (#20477) Fix run-away for mutually referential instance attributes (#20645) [ty] Limit shown import paths to at most 5 unless ty runs with `-v` (#20912) [ty] Use field-specifier return type as the default type for the field (#20915) [ty] Do not assume that `field`s have a default value (#20914) [ty] Fix match pattern value narrowing to use equality semantics (#20882) Update setup instructions for Zed 0.208.0+ (#20902) Move TOML indent size config (#20905) [syntax-errors]: implement F702 as semantic syntax error (#20869) [ty] Heterogeneous unpacking support for unions (#20377) [ty] refactor `Place` (#20871) Auto-accept snapshot changes as part of typeshed-sync PRs (#20892) [`airflow`] Add warning to `airflow.datasets.DatasetEvent` usage (`AIR301`) (#20551) [`flake8-pyi`] Fix operator precedence by adding parentheses when needed (`PYI061`) (#20508) [`pyupgrade`] Fix false negative for `TypeVar` with default argument in `non-pep695-generic-class` (`UP046`) (#20660) Update parser snapshots (#20893) Fix syntax error false positives for escapes and quotes in f-strings (#20867)
Summary
This PR adds precise heterogeneous unpacking support for unions.
For a tuple such as
tuple[int, str], we've long recognised that if you unpack this tuple, the first element will be anintand the second will be astr. But the same has not been true fortuple[Literal[42], str] | tuple[Literal[56], str]-- if a user unpacked this union of tuples, we would infer that both first and second elements were of typeLiteral[42, 56] | str. This PR fixes that: we now infer that the first element will be of typeLiteral[42, 56]and the second element will be of typestr.This doesn't add much complexity to our iteration logic, fixes a number of false positives in the ecosystem, and (surprisingly!) leads to a nice performance boost on the colour-science benchmark.
Test Plan
Mdtests added