Skip to content

Commit 1fee6c3

Browse files
committed
[ty] Fix Todo type for starred elements in tuple expressions
1 parent 665f680 commit 1fee6c3

File tree

8 files changed

+184
-39
lines changed

8 files changed

+184
-39
lines changed

crates/ty_python_semantic/resources/mdtest/bidirectional.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ def f[T](x: T, cond: bool) -> T | list[T]:
4242
return x if cond else [x]
4343

4444
l5: int | list[int] = f(1, True)
45+
46+
a: list[int] = [1, 2, *(3, 4, 5)]
47+
reveal_type(a) # revealed: list[int]
48+
49+
b: list[list[int]] = [[1], [2], *([3], [4])]
50+
reveal_type(b) # revealed: list[list[int]]
4551
```
4652

4753
`typed_dict.py`:

crates/ty_python_semantic/resources/mdtest/expression/len.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ reveal_type(len((1,))) # revealed: Literal[1]
4343
reveal_type(len((1, 2))) # revealed: Literal[2]
4444
reveal_type(len(tuple())) # revealed: Literal[0]
4545

46-
# TODO: Handle star unpacks; Should be: Literal[0]
47-
reveal_type(len((*[],))) # revealed: Literal[1]
46+
# could also be `Literal[0]`, but `int` is accurate
47+
reveal_type(len((*[],))) # revealed: int
4848

4949
# fmt: off
5050

51-
# TODO: Handle star unpacks; Should be: Literal[1]
52-
reveal_type(len( # revealed: Literal[2]
51+
# could also be `Literal[1]`, but `int` is accurate
52+
reveal_type(len( # revealed: int
5353
(
5454
*[],
5555
1,
@@ -58,11 +58,11 @@ reveal_type(len( # revealed: Literal[2]
5858

5959
# fmt: on
6060

61-
# TODO: Handle star unpacks; Should be: Literal[2]
62-
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
61+
# Could also be `Literal[2]`, but `int` is accurate
62+
reveal_type(len((*[], 1, 2))) # revealed: int
6363

64-
# TODO: Handle star unpacks; Should be: Literal[0]
65-
reveal_type(len((*[], *{}))) # revealed: Literal[2]
64+
# Could also be `Literal[0]`, but `int` is accurate
65+
reveal_type(len((*[], *{}))) # revealed: int
6666
```
6767

6868
Tuple subclasses:

crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,4 +531,18 @@ x: list[Literal[1, 2, 3]] = list((1, 2, 3))
531531
reveal_type(x) # revealed: list[Literal[1, 2, 3]]
532532
```
533533

534+
## Tuples with starred elements
535+
536+
```py
537+
x = (1, *range(3), 3)
538+
reveal_type(x) # revealed: tuple[Literal[1], *tuple[int, ...], Literal[3]]
539+
540+
y = 1, 2
541+
542+
reveal_type(("foo", *y)) # revealed: tuple[Literal["foo"], Literal[1], Literal[2]]
543+
544+
aa: tuple[list[int], ...] = ([42], *{[56], [78]}, [100])
545+
reveal_type(aa) # revealed: tuple[list[int], *tuple[list[int], ...], list[int]]
546+
```
547+
534548
[not a singleton type]: https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2941,7 +2941,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
29412941
) {
29422942
let parameters = self.signature.parameters();
29432943
let parameter = &parameters[parameter_index];
2944-
if let Some(mut expected_ty) = parameter.annotated_type() {
2944+
2945+
// TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]`
2946+
if let Some(mut expected_ty) = parameter.annotated_type()
2947+
&& !parameter.has_starred_annotation()
2948+
{
29452949
if let Some(specialization) = self.specialization {
29462950
argument_type = argument_type.apply_specialization(self.db, specialization);
29472951
expected_ty = expected_ty.apply_specialization(self.db, specialization);

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,27 @@ impl<'db> TypeContext<'db> {
377377
annotation: self.annotation.map(f),
378378
}
379379
}
380+
381+
pub(crate) fn for_starred_expression(
382+
db: &'db dyn Db,
383+
expected_element_type: Type<'db>,
384+
expr: &ast::ExprStarred,
385+
) -> Self {
386+
match &*expr.value {
387+
ast::Expr::List(_) => Self::new(Some(
388+
KnownClass::List.to_specialized_instance(db, [expected_element_type]),
389+
)),
390+
ast::Expr::Set(_) => Self::new(Some(
391+
KnownClass::Set.to_specialized_instance(db, [expected_element_type]),
392+
)),
393+
ast::Expr::Tuple(_) => {
394+
Self::new(Some(Type::homogeneous_tuple(db, expected_element_type)))
395+
}
396+
// `Iterable[<expected_element_type>]` would work well for an arbitrary other node
397+
// if <https://github.com/astral-sh/ty/issues/1576> is implemented.
398+
_ => Self::default(),
399+
}
400+
}
380401
}
381402

382403
/// Returns the statically-known truthiness of a given expression.

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ use crate::types::mro::MroErrorKind;
9494
use crate::types::newtype::NewType;
9595
use crate::types::signatures::Signature;
9696
use crate::types::subclass_of::SubclassOfInner;
97-
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
97+
use crate::types::tuple::{
98+
Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType, VariableLengthTuple,
99+
};
98100
use crate::types::typed_dict::{
99101
TypedDictAssignmentKind, validate_typed_dict_constructor, validate_typed_dict_dict_literal,
100102
validate_typed_dict_key_assignment,
@@ -6926,7 +6928,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
69266928
ast::Expr::If(if_expression) => self.infer_if_expression(if_expression, tcx),
69276929
ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression),
69286930
ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression, tcx),
6929-
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
6931+
ast::Expr::Starred(starred) => self.infer_starred_expression(starred, tcx),
69306932
ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression),
69316933
ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from),
69326934
ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression),
@@ -7151,25 +7153,66 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
71517153
)
71527154
});
71537155

7156+
let mut is_homogeneous_tuple_annotation = false;
7157+
71547158
let annotated_tuple = tcx
71557159
.known_specialization(self.db(), KnownClass::Tuple)
71567160
.and_then(|specialization| {
7157-
specialization
7161+
let spec = specialization
71587162
.tuple(self.db())
7159-
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec")
7160-
.resize(self.db(), TupleLength::Fixed(elts.len()))
7161-
.ok()
7163+
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec");
7164+
7165+
if matches!(
7166+
spec,
7167+
Tuple::Variable(VariableLengthTuple { prefix, variable: _, suffix})
7168+
if prefix.is_empty() && suffix.is_empty()
7169+
) {
7170+
is_homogeneous_tuple_annotation = true;
7171+
}
7172+
7173+
spec.resize(self.db(), TupleLength::Fixed(elts.len())).ok()
71627174
});
71637175

71647176
let mut annotated_elt_tys = annotated_tuple.as_ref().map(Tuple::all_elements);
71657177

71667178
let db = self.db();
7167-
let element_types = elts.iter().map(|element| {
7168-
let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied();
7169-
self.infer_expression(element, TypeContext::new(annotated_elt_ty))
7170-
});
71717179

7172-
Type::heterogeneous_tuple(db, element_types)
7180+
let can_use_type_context =
7181+
is_homogeneous_tuple_annotation || elts.iter().all(|elt| !elt.is_starred_expr());
7182+
7183+
let mut infer_element = |elt: &ast::Expr| {
7184+
if can_use_type_context {
7185+
let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied();
7186+
let context = if let ast::Expr::Starred(starred) = elt {
7187+
annotated_elt_ty
7188+
.map(|expected_element_type| {
7189+
TypeContext::for_starred_expression(db, expected_element_type, starred)
7190+
})
7191+
.unwrap_or_default()
7192+
} else {
7193+
TypeContext::new(annotated_elt_ty)
7194+
};
7195+
self.infer_expression(elt, context)
7196+
} else {
7197+
self.infer_expression(elt, TypeContext::default())
7198+
}
7199+
};
7200+
7201+
let mut builder = TupleSpecBuilder::with_capacity(elts.len());
7202+
7203+
for element in elts {
7204+
if element.is_starred_expr() {
7205+
let element_type = infer_element(element);
7206+
// Fine to use `iterate` rather than `try_iterate` here:
7207+
// errors from iterating over something not iterable will have been
7208+
// emitted in the `infer_element` call above.
7209+
builder = builder.concat(db, &element_type.iterate(db));
7210+
} else {
7211+
builder.push(infer_element(element).fallback_to_divergent(db));
7212+
}
7213+
}
7214+
7215+
Type::tuple(TupleType::new(db, &builder.build()))
71737216
}
71747217

71757218
fn infer_list_expression(&mut self, list: &ast::ExprList, tcx: TypeContext<'db>) -> Type<'db> {
@@ -7326,7 +7369,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
73267369

73277370
let inferable = generic_context.inferable_typevars(self.db());
73287371

7329-
// Remove any union elements of that are unrelated to the collection type.
7372+
// Remove any union elements of the annotation that are unrelated to the collection type.
73307373
//
73317374
// For example, we only want the `list[int]` from `annotation: list[int] | None` if
73327375
// `collection_ty` is `list`.
@@ -7366,8 +7409,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
73667409
}
73677410

73687411
let elt_tcxs = match annotated_elt_tys {
7369-
None => Either::Left(iter::repeat(TypeContext::default())),
7370-
Some(tys) => Either::Right(tys.iter().map(|ty| TypeContext::new(Some(*ty)))),
7412+
None => Either::Left(iter::repeat(None)),
7413+
Some(tys) => Either::Right(tys.iter().copied().map(Some)),
73717414
};
73727415

73737416
for elts in elts {
@@ -7396,6 +7439,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
73967439
{
73977440
let Some(elt) = elt else { continue };
73987441

7442+
let elt_tcx = if let ast::Expr::Starred(starred) = elt {
7443+
elt_tcx
7444+
.map(|ty| TypeContext::for_starred_expression(self.db(), ty, starred))
7445+
.unwrap_or_default()
7446+
} else {
7447+
TypeContext::new(elt_tcx)
7448+
};
7449+
73997450
let inferred_elt_ty = infer_elt_expression(self, elt, elt_tcx);
74007451

74017452
// Simplify the inference based on the declared type of the element.
@@ -7409,7 +7460,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
74097460
// unions for large nested list literals, which the constraint solver struggles with.
74107461
let inferred_elt_ty = inferred_elt_ty.promote_literals(self.db(), elt_tcx);
74117462

7412-
builder.infer(Type::TypeVar(elt_ty), inferred_elt_ty).ok()?;
7463+
builder
7464+
.infer(
7465+
Type::TypeVar(elt_ty),
7466+
if elt.is_starred_expr() {
7467+
inferred_elt_ty
7468+
.iterate(self.db())
7469+
.homogeneous_element_type(self.db())
7470+
} else {
7471+
inferred_elt_ty
7472+
},
7473+
)
7474+
.ok()?;
74137475
}
74147476
}
74157477

@@ -8204,25 +8266,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
82048266
}
82058267
}
82068268

8207-
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
8269+
fn infer_starred_expression(
8270+
&mut self,
8271+
starred: &ast::ExprStarred,
8272+
tcx: TypeContext<'db>,
8273+
) -> Type<'db> {
82088274
let ast::ExprStarred {
82098275
range: _,
82108276
node_index: _,
82118277
value,
82128278
ctx: _,
82138279
} = starred;
82148280

8215-
let iterable_type = self.infer_expression(value, TypeContext::default());
8281+
let db = self.db();
8282+
let iterable_type = self.infer_expression(value, tcx);
8283+
82168284
iterable_type
8217-
.try_iterate(self.db())
8218-
.map(|tuple| tuple.homogeneous_element_type(self.db()))
8285+
.try_iterate(db)
8286+
.map(|spec| Type::tuple(TupleType::new(db, &spec)))
82198287
.unwrap_or_else(|err| {
82208288
err.report_diagnostic(&self.context, iterable_type, value.as_ref().into());
8221-
err.fallback_element_type(self.db())
8222-
});
8223-
8224-
// TODO
8225-
todo_type!("starred expression")
8289+
Type::homogeneous_tuple(db, err.fallback_element_type(db))
8290+
})
82268291
}
82278292

82288293
fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> {

crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
121121
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
122122

123123
// Annotation expressions also get special handling for `*args` and `**kwargs`.
124-
ast::Expr::Starred(starred) => {
125-
TypeAndQualifiers::declared(self.infer_starred_expression(starred))
126-
}
124+
ast::Expr::Starred(starred) => TypeAndQualifiers::declared(
125+
self.infer_starred_expression(starred, TypeContext::default()),
126+
),
127127

128128
ast::Expr::BytesLiteral(bytes) => {
129129
if let Some(builder) = self

0 commit comments

Comments
 (0)