Skip to content

Commit ec3d5eb

Browse files
[ty] Upcast heterogeneous and mixed tuples to homogeneous tuples where it's necessary to solve a TypeVar (#19635)
## Summary This PR improves our generics solver such that we are able to solve the `TypeVar` in this snippet to `int | str` (the union of the elements in the heterogeneous tuple) by upcasting the heterogeneous tuple to its pure-homogeneous-tuple supertype: ```py def f[T](x: tuple[T, ...]) -> T: return x[0] def g(x: tuple[int, str]): reveal_type(f(x)) ``` ## Test Plan Mdtests. Some TODOs remain in the mdtest regarding solving `TypeVar`s for mixed tuples, but I think this PR on its own is a significant step forward for our generics solver when it comes to tuple types. --------- Co-authored-by: Douglas Creager <dcreager@dcreager.net>
1 parent d797592 commit ec3d5eb

4 files changed

Lines changed: 72 additions & 29 deletions

File tree

crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,34 @@ T = TypeVar("T")
145145
def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
146146
return x[-2]
147147

148-
# TODO: revealed: Literal[True]
149-
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
150-
151148
def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
152149
return x[1]
153150

154-
# TODO: revealed: Literal[b"foo"]
155-
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
151+
def _(x: tuple[int, bytes, *tuple[str, ...], bool, int]):
152+
reveal_type(takes_mixed_tuple_suffix(x)) # revealed: bool
153+
reveal_type(takes_mixed_tuple_prefix(x)) # revealed: bytes
154+
155+
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[True]
156+
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[b"foo"]
156157

157158
def takes_fixed_tuple(x: tuple[T, int]) -> T:
158159
return x[0]
159160

161+
def _(x: tuple[str, int]):
162+
reveal_type(takes_fixed_tuple(x)) # revealed: str
163+
160164
reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]
161165

162166
def takes_homogeneous_tuple(x: tuple[T, ...]) -> T:
163167
return x[0]
164168

165-
# TODO: revealed: Literal[42]
166-
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
167-
# TODO: revealed: Literal[42, 43]
168-
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
169+
def _(x: tuple[str, int], y: tuple[bool, ...], z: tuple[int, str, *tuple[range, ...], bytes]):
170+
reveal_type(takes_homogeneous_tuple(x)) # revealed: str | int
171+
reveal_type(takes_homogeneous_tuple(y)) # revealed: bool
172+
reveal_type(takes_homogeneous_tuple(z)) # revealed: int | str | range | bytes
173+
174+
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Literal[42]
175+
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43]
169176
```
170177

171178
## Inferring a bound typevar

crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,27 +131,34 @@ reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str
131131
def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
132132
return x[-2]
133133

134-
# TODO: revealed: Literal[True]
135-
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
136-
137134
def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
138135
return x[1]
139136

140-
# TODO: revealed: Literal[b"foo"]
141-
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
137+
def _(x: tuple[int, bytes, *tuple[str, ...], bool, int]):
138+
reveal_type(takes_mixed_tuple_suffix(x)) # revealed: bool
139+
reveal_type(takes_mixed_tuple_prefix(x)) # revealed: bytes
140+
141+
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[True]
142+
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[b"foo"]
142143

143144
def takes_fixed_tuple[T](x: tuple[T, int]) -> T:
144145
return x[0]
145146

147+
def _(x: tuple[str, int]):
148+
reveal_type(takes_fixed_tuple(x)) # revealed: str
149+
146150
reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]
147151

148152
def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T:
149153
return x[0]
150154

151-
# TODO: revealed: Literal[42]
152-
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
153-
# TODO: revealed: Literal[42, 43]
154-
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
155+
def _(x: tuple[str, int], y: tuple[bool, ...], z: tuple[int, str, *tuple[range, ...], bytes]):
156+
reveal_type(takes_homogeneous_tuple(x)) # revealed: str | int
157+
reveal_type(takes_homogeneous_tuple(y)) # revealed: bool
158+
reveal_type(takes_homogeneous_tuple(z)) # revealed: int | str | range | bytes
159+
160+
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Literal[42]
161+
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43]
155162
```
156163

157164
## Inferring a bound typevar

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -761,17 +761,19 @@ impl<'db> SpecializationBuilder<'db> {
761761
(Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => {
762762
let formal_tuple = formal_tuple.tuple(self.db);
763763
let actual_tuple = actual_tuple.tuple(self.db);
764-
match (formal_tuple, actual_tuple) {
765-
(TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => {
766-
if formal_tuple.len() == actual_tuple.len() {
767-
for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) {
768-
self.infer(*formal_element, *actual_element)?;
769-
}
770-
}
771-
}
772-
773-
// TODO: Infer specializations of variable-length tuples
774-
(TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) => {}
764+
let Some(most_precise_length) = formal_tuple.len().most_precise(actual_tuple.len()) else {
765+
return Ok(());
766+
};
767+
let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else {
768+
return Ok(());
769+
};
770+
let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else {
771+
return Ok(());
772+
};
773+
for (formal_element, actual_element) in
774+
formal_tuple.all_elements().zip(actual_tuple.all_elements())
775+
{
776+
self.infer(*formal_element, *actual_element)?;
775777
}
776778
}
777779

crates/ty_python_semantic/src/types/tuple.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,33 @@ impl TupleLength {
7171
}
7272
}
7373

74+
/// Given two [`TupleLength`]s, return the more precise instance,
75+
/// if it makes sense to consider one more precise than the other.
76+
pub(crate) fn most_precise(self, other: Self) -> Option<Self> {
77+
match (self, other) {
78+
// A fixed-length tuple is equally as precise as another fixed-length tuple if they
79+
// have the same length. For two differently sized fixed-length tuples, however,
80+
// neither tuple length is more precise than the other: the two tuple lengths are
81+
// entirely disjoint.
82+
(TupleLength::Fixed(left), TupleLength::Fixed(right)) => {
83+
(left == right).then_some(self)
84+
}
85+
86+
// A fixed-length tuple is more precise than a variable-length one.
87+
(fixed @ TupleLength::Fixed(_), TupleLength::Variable(..))
88+
| (TupleLength::Variable(..), fixed @ TupleLength::Fixed(_)) => Some(fixed),
89+
90+
// For two variable-length tuples, the tuple with the larger number
91+
// of required items is more precise.
92+
(TupleLength::Variable(..), TupleLength::Variable(..)) => {
93+
Some(match self.minimum().cmp(&other.minimum()) {
94+
Ordering::Less => other,
95+
Ordering::Equal | Ordering::Greater => self,
96+
})
97+
}
98+
}
99+
}
100+
74101
pub(crate) fn display_minimum(self) -> String {
75102
let minimum_length = self.minimum();
76103
match self {

0 commit comments

Comments
 (0)