Skip to content

Commit c60e590

Browse files
authored
[ty] Support variable-length tuples in unpacking assignments (astral-sh#18948)
This PR updates our unpacking assignment logic to use the new tuple machinery. As a result, we can now unpack variable-length tuples correctly. As part of this, the `TupleSpec` classes have been renamed to `Tuple`, and can now contain any element (Rust) type, not just `Type<'db>`. The unpacker uses a tuple of `UnionBuilder`s to maintain the types that will be assigned to each target, as we iterate through potentially many union elements on the rhs. We also add a new consuming iterator for tuples, and update the `all_elements` methods to wrap the result in an enum (similar to `itertools::Position`) letting you know which part of the tuple each element appears in. I also added a new `UnionBuilder::try_build`, which lets you specify a different fallback type if the union contains no elements.
1 parent a50a993 commit c60e590

File tree

11 files changed

+779
-423
lines changed

11 files changed

+779
-423
lines changed

crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_un…_(cef19e6b2b58e6a3).snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ error[invalid-assignment]: Not enough values to unpack
2424
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
2525
| ^^^^^^^^^^^^^ ------ Got 2
2626
| |
27-
| Expected 3 or more
27+
| Expected at least 3
2828
|
2929
info: rule `invalid-assignment` is enabled by default
3030

crates/ty_python_semantic/resources/mdtest/unpacking.md

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ reveal_type(d) # revealed: Literal[5]
106106
### Starred expression (1)
107107

108108
```py
109-
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
109+
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
110110
[a, *b, c, d] = (1, 2)
111111
reveal_type(a) # revealed: Unknown
112112
reveal_type(b) # revealed: list[Unknown]
@@ -119,7 +119,7 @@ reveal_type(d) # revealed: Unknown
119119
```py
120120
[a, *b, c] = (1, 2)
121121
reveal_type(a) # revealed: Literal[1]
122-
reveal_type(b) # revealed: list[Unknown]
122+
reveal_type(b) # revealed: list[Never]
123123
reveal_type(c) # revealed: Literal[2]
124124
```
125125

@@ -154,7 +154,7 @@ reveal_type(c) # revealed: list[Literal[3, 4]]
154154
### Starred expression (6)
155155

156156
```py
157-
# error: [invalid-assignment] "Not enough values to unpack: Expected 5 or more"
157+
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 5"
158158
(a, b, c, *d, e, f) = (1,)
159159
reveal_type(a) # revealed: Unknown
160160
reveal_type(b) # revealed: Unknown
@@ -258,6 +258,155 @@ def _(value: list[int]):
258258
reveal_type(c) # revealed: int
259259
```
260260

261+
## Homogeneous tuples
262+
263+
### Simple unpacking
264+
265+
```py
266+
def _(value: tuple[int, ...]):
267+
a, b = value
268+
reveal_type(a) # revealed: int
269+
reveal_type(b) # revealed: int
270+
```
271+
272+
### Nested unpacking
273+
274+
```py
275+
def _(value: tuple[tuple[int, ...], ...]):
276+
a, (b, c) = value
277+
reveal_type(a) # revealed: tuple[int, ...]
278+
reveal_type(b) # revealed: int
279+
reveal_type(c) # revealed: int
280+
```
281+
282+
### Invalid nested unpacking
283+
284+
```py
285+
def _(value: tuple[int, ...]):
286+
# error: [not-iterable] "Object of type `int` is not iterable"
287+
a, (b, c) = value
288+
reveal_type(a) # revealed: int
289+
reveal_type(b) # revealed: Unknown
290+
reveal_type(c) # revealed: Unknown
291+
```
292+
293+
### Starred expression
294+
295+
```py
296+
def _(value: tuple[int, ...]):
297+
a, *b, c = value
298+
reveal_type(a) # revealed: int
299+
reveal_type(b) # revealed: list[int]
300+
reveal_type(c) # revealed: int
301+
```
302+
303+
## Mixed tuples
304+
305+
```toml
306+
[environment]
307+
python-version = "3.11"
308+
```
309+
310+
### Simple unpacking (1)
311+
312+
```py
313+
def _(value: tuple[int, *tuple[str, ...]]):
314+
a, b = value
315+
reveal_type(a) # revealed: int
316+
reveal_type(b) # revealed: str
317+
```
318+
319+
### Simple unpacking (2)
320+
321+
```py
322+
def _(value: tuple[int, int, *tuple[str, ...]]):
323+
a, b = value
324+
reveal_type(a) # revealed: int
325+
reveal_type(b) # revealed: int
326+
```
327+
328+
### Simple unpacking (3)
329+
330+
```py
331+
def _(value: tuple[int, *tuple[str, ...], int]):
332+
a, b, c = value
333+
reveal_type(a) # revealed: int
334+
reveal_type(b) # revealed: str
335+
reveal_type(c) # revealed: int
336+
```
337+
338+
### Invalid unpacked
339+
340+
```py
341+
def _(value: tuple[int, int, int, *tuple[str, ...]]):
342+
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
343+
a, b = value
344+
reveal_type(a) # revealed: Unknown
345+
reveal_type(b) # revealed: Unknown
346+
```
347+
348+
### Nested unpacking
349+
350+
```py
351+
def _(value: tuple[str, *tuple[tuple[int, ...], ...]]):
352+
a, (b, c) = value
353+
reveal_type(a) # revealed: str
354+
reveal_type(b) # revealed: int
355+
reveal_type(c) # revealed: int
356+
```
357+
358+
### Invalid nested unpacking
359+
360+
```py
361+
def _(value: tuple[str, *tuple[int, ...]]):
362+
# error: [not-iterable] "Object of type `int` is not iterable"
363+
a, (b, c) = value
364+
reveal_type(a) # revealed: str
365+
reveal_type(b) # revealed: Unknown
366+
reveal_type(c) # revealed: Unknown
367+
```
368+
369+
### Starred expression (1)
370+
371+
```py
372+
def _(value: tuple[int, *tuple[str, ...]]):
373+
a, *b, c = value
374+
reveal_type(a) # revealed: int
375+
reveal_type(b) # revealed: list[str]
376+
reveal_type(c) # revealed: str
377+
```
378+
379+
### Starred expression (2)
380+
381+
```py
382+
def _(value: tuple[int, *tuple[str, ...], int]):
383+
a, *b, c = value
384+
reveal_type(a) # revealed: int
385+
reveal_type(b) # revealed: list[str]
386+
reveal_type(c) # revealed: int
387+
```
388+
389+
### Starred expression (3)
390+
391+
```py
392+
def _(value: tuple[int, *tuple[str, ...], int]):
393+
a, *b, c, d = value
394+
reveal_type(a) # revealed: int
395+
reveal_type(b) # revealed: list[str]
396+
reveal_type(c) # revealed: str
397+
reveal_type(d) # revealed: int
398+
```
399+
400+
### Starred expression (4)
401+
402+
```py
403+
def _(value: tuple[int, int, *tuple[str, ...], int]):
404+
a, *b, c = value
405+
reveal_type(a) # revealed: int
406+
reveal_type(b) # revealed: list[int | str]
407+
reveal_type(c) # revealed: int
408+
```
409+
261410
## String
262411

263412
### Simple unpacking
@@ -290,7 +439,7 @@ reveal_type(b) # revealed: Unknown
290439
### Starred expression (1)
291440

292441
```py
293-
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
442+
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
294443
(a, *b, c, d) = "ab"
295444
reveal_type(a) # revealed: Unknown
296445
reveal_type(b) # revealed: list[Unknown]
@@ -299,7 +448,7 @@ reveal_type(d) # revealed: Unknown
299448
```
300449

301450
```py
302-
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
451+
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
303452
(a, b, *c, d) = "a"
304453
reveal_type(a) # revealed: Unknown
305454
reveal_type(b) # revealed: Unknown
@@ -312,7 +461,7 @@ reveal_type(d) # revealed: Unknown
312461
```py
313462
(a, *b, c) = "ab"
314463
reveal_type(a) # revealed: LiteralString
315-
reveal_type(b) # revealed: list[Unknown]
464+
reveal_type(b) # revealed: list[Never]
316465
reveal_type(c) # revealed: LiteralString
317466
```
318467

crates/ty_python_semantic/src/types.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ impl<'db> Type<'db> {
726726
.map(|ty| ty.materialize(db, variance.flip())),
727727
)
728728
.build(),
729-
Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.materialize(db, variance)),
729+
Type::Tuple(tuple_type) => Type::tuple(tuple_type.materialize(db, variance)),
730730
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
731731
Type::TypeIs(type_is) => {
732732
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
@@ -1141,7 +1141,7 @@ impl<'db> Type<'db> {
11411141
match self {
11421142
Type::Union(union) => Type::Union(union.normalized(db)),
11431143
Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)),
1144-
Type::Tuple(tuple) => Type::tuple(db, tuple.normalized(db)),
1144+
Type::Tuple(tuple) => Type::tuple(tuple.normalized(db)),
11451145
Type::Callable(callable) => Type::Callable(callable.normalized(db)),
11461146
Type::ProtocolInstance(protocol) => protocol.normalized(db),
11471147
Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)),
@@ -3458,7 +3458,7 @@ impl<'db> Type<'db> {
34583458
Type::BooleanLiteral(bool) => Truthiness::from(*bool),
34593459
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
34603460
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
3461-
Type::Tuple(tuple) => match tuple.tuple(db).size_hint() {
3461+
Type::Tuple(tuple) => match tuple.tuple(db).len().size_hint() {
34623462
// The tuple type is AlwaysFalse if it contains only the empty tuple
34633463
(_, Some(0)) => Truthiness::AlwaysFalse,
34643464
// The tuple type is AlwaysTrue if its inhabitants must always have length >=1
@@ -4312,7 +4312,7 @@ impl<'db> Type<'db> {
43124312
let mut parameter =
43134313
Parameter::positional_only(Some(Name::new_static("iterable")))
43144314
.with_annotated_type(instantiated);
4315-
if matches!(spec.size_hint().1, Some(0)) {
4315+
if matches!(spec.len().maximum(), Some(0)) {
43164316
parameter = parameter.with_default_type(TupleType::empty(db));
43174317
}
43184318
Parameters::new([parameter])
@@ -5350,7 +5350,7 @@ impl<'db> Type<'db> {
53505350
}
53515351
builder.build()
53525352
}
5353-
Type::Tuple(tuple) => Type::Tuple(tuple.apply_type_mapping(db, type_mapping)),
5353+
Type::Tuple(tuple) => Type::tuple(tuple.apply_type_mapping(db, type_mapping)),
53545354

53555355
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)),
53565356

crates/ty_python_semantic/src/types/builder.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@ impl<'db> UnionBuilder<'db> {
444444
}
445445

446446
pub(crate) fn build(self) -> Type<'db> {
447+
self.try_build().unwrap_or(Type::Never)
448+
}
449+
450+
pub(crate) fn try_build(self) -> Option<Type<'db>> {
447451
let mut types = vec![];
448452
for element in self.elements {
449453
match element {
@@ -460,9 +464,12 @@ impl<'db> UnionBuilder<'db> {
460464
}
461465
}
462466
match types.len() {
463-
0 => Type::Never,
464-
1 => types[0],
465-
_ => Type::Union(UnionType::new(self.db, types.into_boxed_slice())),
467+
0 => None,
468+
1 => Some(types[0]),
469+
_ => Some(Type::Union(UnionType::new(
470+
self.db,
471+
types.into_boxed_slice(),
472+
))),
466473
}
467474
}
468475
}

crates/ty_python_semantic/src/types/call/arguments.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,10 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
221221
let expanded = tuple
222222
.all_elements()
223223
.map(|element| {
224-
if let Some(expanded) = expand_type(db, element) {
224+
if let Some(expanded) = expand_type(db, *element) {
225225
Either::Left(expanded.into_iter())
226226
} else {
227-
Either::Right(std::iter::once(element))
227+
Either::Right(std::iter::once(*element))
228228
}
229229
})
230230
.multi_cartesian_product()

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,13 @@ impl<'db> Specialization<'db> {
286286
return tuple;
287287
}
288288
if let [element_type] = self.types(db) {
289-
return TupleType::new(db, TupleSpec::homogeneous(*element_type)).tuple(db);
289+
if let Some(tuple) = TupleType::new(db, TupleSpec::homogeneous(*element_type)) {
290+
return tuple.tuple(db);
291+
}
290292
}
291-
TupleType::new(db, TupleSpec::homogeneous(Type::unknown())).tuple(db)
293+
TupleType::new(db, TupleSpec::homogeneous(Type::unknown()))
294+
.expect("tuple[Unknown, ...] should never contain Never")
295+
.tuple(db)
292296
}
293297

294298
/// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this
@@ -330,7 +334,7 @@ impl<'db> Specialization<'db> {
330334
.collect();
331335
let tuple_inner = self
332336
.tuple_inner(db)
333-
.map(|tuple| tuple.apply_type_mapping(db, type_mapping));
337+
.and_then(|tuple| tuple.apply_type_mapping(db, type_mapping));
334338
Specialization::new(db, self.generic_context(db), types, tuple_inner)
335339
}
336340

@@ -374,7 +378,7 @@ impl<'db> Specialization<'db> {
374378

375379
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
376380
let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect();
377-
let tuple_inner = self.tuple_inner(db).map(|tuple| tuple.normalized(db));
381+
let tuple_inner = self.tuple_inner(db).and_then(|tuple| tuple.normalized(db));
378382
Self::new(db, self.generic_context(db), types, tuple_inner)
379383
}
380384

@@ -394,7 +398,7 @@ impl<'db> Specialization<'db> {
394398
vartype.materialize(db, variance)
395399
})
396400
.collect();
397-
let tuple_inner = self.tuple_inner(db).map(|tuple| {
401+
let tuple_inner = self.tuple_inner(db).and_then(|tuple| {
398402
// Tuples are immutable, so tuple element types are always in covariant position.
399403
tuple.materialize(db, variance)
400404
});
@@ -637,7 +641,7 @@ impl<'db> SpecializationBuilder<'db> {
637641
(TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => {
638642
if formal_tuple.len() == actual_tuple.len() {
639643
for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) {
640-
self.infer(formal_element, actual_element)?;
644+
self.infer(*formal_element, *actual_element)?;
641645
}
642646
}
643647
}

0 commit comments

Comments
 (0)