Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ error[invalid-assignment]: Not enough values to unpack
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
| ^^^^^^^^^^^^^ ------ Got 2
| |
| Expected 3 or more
| Expected at least 3
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These rewordings aren't because I had a strong opinion about the word choice; it's just that we're now using the same display machinery that we were using for e.g. the index-out-of-bounds diagnostics when indexing into a tuple.

|
info: rule `invalid-assignment` is enabled by default

Expand Down
161 changes: 155 additions & 6 deletions crates/ty_python_semantic/resources/mdtest/unpacking.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ reveal_type(d) # revealed: Literal[5]
### Starred expression (1)

```py
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: list[Unknown]
Expand All @@ -119,7 +119,7 @@ reveal_type(d) # revealed: Unknown
```py
[a, *b, c] = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: list[Unknown]
reveal_type(b) # revealed: list[Never]
reveal_type(c) # revealed: Literal[2]
```

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

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

## Homogeneous tuples

### Simple unpacking

```py
def _(value: tuple[int, ...]):
a, b = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```

### Nested unpacking

```py
def _(value: tuple[tuple[int, ...], ...]):
a, (b, c) = value
reveal_type(a) # revealed: tuple[int, ...]
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
```

### Invalid nested unpacking

```py
def _(value: tuple[int, ...]):
# error: [not-iterable] "Object of type `int` is not iterable"
a, (b, c) = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```

### Starred expression

```py
def _(value: tuple[int, ...]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[int]
reveal_type(c) # revealed: int
```

## Mixed tuples

```toml
[environment]
python-version = "3.11"
```

### Simple unpacking (1)

```py
def _(value: tuple[int, *tuple[str, ...]]):
a, b = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
```

### Simple unpacking (2)

```py
def _(value: tuple[int, int, *tuple[str, ...]]):
a, b = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```

### Simple unpacking (3)

```py
def _(value: tuple[int, *tuple[str, ...], int]):
a, b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
reveal_type(c) # revealed: int
```

### Invalid unpacked

```py
def _(value: tuple[int, int, int, *tuple[str, ...]]):
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
a, b = value
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```

### Nested unpacking

```py
def _(value: tuple[str, *tuple[tuple[int, ...], ...]]):
a, (b, c) = value
reveal_type(a) # revealed: str
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
```

### Invalid nested unpacking

```py
def _(value: tuple[str, *tuple[int, ...]]):
# error: [not-iterable] "Object of type `int` is not iterable"
a, (b, c) = value
reveal_type(a) # revealed: str
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```

### Starred expression (1)

```py
def _(value: tuple[int, *tuple[str, ...]]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[str]
reveal_type(c) # revealed: str
```

### Starred expression (2)

```py
def _(value: tuple[int, *tuple[str, ...], int]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[str]
reveal_type(c) # revealed: int
```

### Starred expression (3)

```py
def _(value: tuple[int, *tuple[str, ...], int]):
a, *b, c, d = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[str]
reveal_type(c) # revealed: str
reveal_type(d) # revealed: int
```

### Starred expression (4)

```py
def _(value: tuple[int, int, *tuple[str, ...], int]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[int | str]
reveal_type(c) # revealed: int
```

## String

### Simple unpacking
Expand Down Expand Up @@ -290,7 +439,7 @@ reveal_type(b) # revealed: Unknown
### Starred expression (1)

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

```py
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
(a, b, *c, d) = "a"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
Expand All @@ -312,7 +461,7 @@ reveal_type(d) # revealed: Unknown
```py
(a, *b, c) = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: list[Unknown]
reveal_type(b) # revealed: list[Never]
reveal_type(c) # revealed: LiteralString
```

Expand Down
40 changes: 25 additions & 15 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::tuple::{TupleElement, TupleSpec, TupleType};
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
Expand Down Expand Up @@ -721,7 +721,9 @@ impl<'db> Type<'db> {
.map(|ty| ty.materialize(db, variance.flip())),
)
.build(),
Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.materialize(db, variance)),
Type::Tuple(tuple_type) => {
Type::tuple(db, tuple_type.tuple(db).materialize(db, variance))
}
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
Type::TypeIs(type_is) => {
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
Expand Down Expand Up @@ -767,7 +769,7 @@ impl<'db> Type<'db> {
tuple
.tuple(db)
.all_elements()
.map(|ty| ty.replace_self_reference(db, class)),
.map(|ty| ty.into_inner().replace_self_reference(db, class)),
),

Self::Callable(callable) => Self::Callable(callable.replace_self_reference(db, class)),
Expand Down Expand Up @@ -878,7 +880,7 @@ impl<'db> Type<'db> {
Self::Tuple(tuple) => tuple
.tuple(db)
.all_elements()
.any(|ty| ty.any_over_type(db, type_fn)),
.any(|ty| ty.into_inner().any_over_type(db, type_fn)),

Self::Union(union) => union
.elements(db)
Expand Down Expand Up @@ -1129,7 +1131,7 @@ impl<'db> Type<'db> {
match self {
Type::Union(union) => Type::Union(union.normalized(db)),
Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)),
Type::Tuple(tuple) => Type::tuple(db, tuple.normalized(db)),
Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.tuple(db).normalized(db)),
Type::Callable(callable) => Type::Callable(callable.normalized(db)),
Type::ProtocolInstance(protocol) => protocol.normalized(db),
Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)),
Expand Down Expand Up @@ -3446,14 +3448,19 @@ impl<'db> Type<'db> {
Type::BooleanLiteral(bool) => Truthiness::from(*bool),
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
Type::Tuple(tuple) => match tuple.tuple(db).size_hint() {
// The tuple type is AlwaysFalse if it contains only the empty tuple
(_, Some(0)) => Truthiness::AlwaysFalse,
// The tuple type is AlwaysTrue if its inhabitants must always have length >=1
(minimum, _) if minimum > 0 => Truthiness::AlwaysTrue,
// The tuple type is Ambiguous if its inhabitants could be of any length
_ => Truthiness::Ambiguous,
},
Type::Tuple(tuple) => {
let len = tuple.tuple(db).len();
if len.maximum() == Some(0) {
// The tuple type is AlwaysFalse if it contains only the empty tuple
Truthiness::AlwaysFalse
} else if len.minimum() > 0 {
// The tuple type is AlwaysTrue if its inhabitants must always have length >=1
Truthiness::AlwaysTrue
} else {
// The tuple type is Ambiguous if its inhabitants could be of any length
Truthiness::Ambiguous
}
}
};

Ok(truthiness)
Expand Down Expand Up @@ -4453,7 +4460,10 @@ impl<'db> Type<'db> {
if let Type::Tuple(tuple_type) = self {
return Ok(UnionType::from_elements(
db,
tuple_type.tuple(db).all_elements(),
tuple_type
.tuple(db)
.all_elements()
.map(TupleElement::into_inner),
));
}

Expand Down Expand Up @@ -5307,7 +5317,7 @@ impl<'db> Type<'db> {
}
builder.build()
}
Type::Tuple(tuple) => Type::Tuple(tuple.apply_type_mapping(db, type_mapping)),
Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.tuple(db).apply_type_mapping(db, type_mapping)),

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

Expand Down
13 changes: 10 additions & 3 deletions crates/ty_python_semantic/src/types/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ impl<'db> UnionBuilder<'db> {
}

pub(crate) fn build(self) -> Type<'db> {
self.try_build().unwrap_or(Type::Never)
}

pub(crate) fn try_build(self) -> Option<Type<'db>> {
let mut types = vec![];
for element in self.elements {
match element {
Expand All @@ -460,9 +464,12 @@ impl<'db> UnionBuilder<'db> {
}
}
match types.len() {
0 => Type::Never,
1 => types[0],
_ => Type::Union(UnionType::new(self.db, types.into_boxed_slice())),
0 => None,
1 => Some(types[0]),
_ => Some(Type::Union(UnionType::new(
self.db,
types.into_boxed_slice(),
))),
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions crates/ty_python_semantic/src/types/call/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,11 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
let expanded = tuple
.all_elements()
.map(|element| {
if let Some(expanded) = expand_type(db, element) {
let element = element.into_inner();
if let Some(expanded) = expand_type(db, *element) {
Either::Left(expanded.into_iter())
} else {
Either::Right(std::iter::once(element))
Either::Right(std::iter::once(*element))
}
})
.multi_cartesian_product()
Expand Down
9 changes: 6 additions & 3 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::tuple::{TupleElement, TupleSpec, TupleType};
use crate::types::{
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarVariance, UnionType, declaration_type,
Expand Down Expand Up @@ -181,7 +181,10 @@ impl<'db> GenericContext<'db> {
db: &'db dyn Db,
tuple: TupleType<'db>,
) -> Specialization<'db> {
let element_type = UnionType::from_elements(db, tuple.tuple(db).all_elements());
let element_type = UnionType::from_elements(
db,
tuple.tuple(db).all_elements().map(TupleElement::into_inner),
);
Specialization::new(db, self, Box::from([element_type]), Some(tuple))
}

Expand Down Expand Up @@ -634,7 +637,7 @@ impl<'db> SpecializationBuilder<'db> {
(TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => {
if formal_tuple.len() == actual_tuple.len() {
for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) {
self.infer(formal_element, actual_element)?;
self.infer(*formal_element, *actual_element)?;
}
}
}
Expand Down
Loading
Loading