Skip to content

Commit 93e6023

Browse files
committed
[ty] Fix Todo type for starred elements in tuple expressions
1 parent df66946 commit 93e6023

File tree

8 files changed

+187
-39
lines changed

8 files changed

+187
-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
@@ -2974,7 +2974,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
29742974
) {
29752975
let parameters = self.signature.parameters();
29762976
let parameter = &parameters[parameter_index];
2977-
if let Some(mut expected_ty) = parameter.annotated_type() {
2977+
2978+
// TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]`
2979+
if let Some(mut expected_ty) = parameter.annotated_type()
2980+
&& !parameter.has_starred_annotation()
2981+
{
29782982
if let Some(specialization) = self.specialization {
29792983
argument_type = argument_type.apply_specialization(self.db, specialization);
29802984
expected_ty = expected_ty.apply_specialization(self.db, specialization);

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
//! of iterations, so if we fail to converge, Salsa will eventually panic. (This should of course
3737
//! be considered a bug.)
3838
39+
use ruff_python_ast as ast;
3940
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
4041
use ruff_text_size::Ranged;
4142
use rustc_hash::{FxHashMap, FxHashSet};
@@ -384,6 +385,27 @@ impl<'db> TypeContext<'db> {
384385
self.annotation
385386
.is_some_and(|ty| ty.is_typealias_special_form())
386387
}
388+
389+
pub(crate) fn for_starred_expression(
390+
db: &'db dyn Db,
391+
expected_element_type: Type<'db>,
392+
expr: &ast::ExprStarred,
393+
) -> Self {
394+
match &*expr.value {
395+
ast::Expr::List(_) => Self::new(Some(
396+
KnownClass::List.to_specialized_instance(db, [expected_element_type]),
397+
)),
398+
ast::Expr::Set(_) => Self::new(Some(
399+
KnownClass::Set.to_specialized_instance(db, [expected_element_type]),
400+
)),
401+
ast::Expr::Tuple(_) => {
402+
Self::new(Some(Type::homogeneous_tuple(db, expected_element_type)))
403+
}
404+
// `Iterable[<expected_element_type>]` would work well for an arbitrary other node
405+
// if <https://github.com/astral-sh/ty/issues/1576> is implemented.
406+
_ => Self::default(),
407+
}
408+
}
387409
}
388410

389411
/// 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
@@ -95,7 +95,9 @@ use crate::types::mro::MroErrorKind;
9595
use crate::types::newtype::NewType;
9696
use crate::types::signatures::{Parameter, Parameters, Signature};
9797
use crate::types::subclass_of::SubclassOfInner;
98-
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
98+
use crate::types::tuple::{
99+
Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType, VariableLengthTuple,
100+
};
99101
use crate::types::typed_dict::{
100102
TypedDictAssignmentKind, validate_typed_dict_constructor, validate_typed_dict_dict_literal,
101103
validate_typed_dict_key_assignment,
@@ -7048,7 +7050,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
70487050
ast::Expr::If(if_expression) => self.infer_if_expression(if_expression, tcx),
70497051
ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression),
70507052
ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression, tcx),
7051-
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
7053+
ast::Expr::Starred(starred) => self.infer_starred_expression(starred, tcx),
70527054
ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression),
70537055
ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from),
70547056
ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression),
@@ -7284,25 +7286,66 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
72847286
)
72857287
});
72867288

7289+
let mut is_homogeneous_tuple_annotation = false;
7290+
72877291
let annotated_tuple = tcx
72887292
.known_specialization(self.db(), KnownClass::Tuple)
72897293
.and_then(|specialization| {
7290-
specialization
7294+
let spec = specialization
72917295
.tuple(self.db())
7292-
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec")
7293-
.resize(self.db(), TupleLength::Fixed(elts.len()))
7294-
.ok()
7296+
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec");
7297+
7298+
if matches!(
7299+
spec,
7300+
Tuple::Variable(VariableLengthTuple { prefix, variable: _, suffix})
7301+
if prefix.is_empty() && suffix.is_empty()
7302+
) {
7303+
is_homogeneous_tuple_annotation = true;
7304+
}
7305+
7306+
spec.resize(self.db(), TupleLength::Fixed(elts.len())).ok()
72957307
});
72967308

72977309
let mut annotated_elt_tys = annotated_tuple.as_ref().map(Tuple::all_elements);
72987310

72997311
let db = self.db();
7300-
let element_types = elts.iter().map(|element| {
7301-
let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied();
7302-
self.infer_expression(element, TypeContext::new(annotated_elt_ty))
7303-
});
73047312

7305-
Type::heterogeneous_tuple(db, element_types)
7313+
let can_use_type_context =
7314+
is_homogeneous_tuple_annotation || elts.iter().all(|elt| !elt.is_starred_expr());
7315+
7316+
let mut infer_element = |elt: &ast::Expr| {
7317+
if can_use_type_context {
7318+
let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied();
7319+
let context = if let ast::Expr::Starred(starred) = elt {
7320+
annotated_elt_ty
7321+
.map(|expected_element_type| {
7322+
TypeContext::for_starred_expression(db, expected_element_type, starred)
7323+
})
7324+
.unwrap_or_default()
7325+
} else {
7326+
TypeContext::new(annotated_elt_ty)
7327+
};
7328+
self.infer_expression(elt, context)
7329+
} else {
7330+
self.infer_expression(elt, TypeContext::default())
7331+
}
7332+
};
7333+
7334+
let mut builder = TupleSpecBuilder::with_capacity(elts.len());
7335+
7336+
for element in elts {
7337+
if element.is_starred_expr() {
7338+
let element_type = infer_element(element);
7339+
// Fine to use `iterate` rather than `try_iterate` here:
7340+
// errors from iterating over something not iterable will have been
7341+
// emitted in the `infer_element` call above.
7342+
builder = builder.concat(db, &element_type.iterate(db));
7343+
} else {
7344+
builder.push(infer_element(element));
7345+
}
7346+
}
7347+
7348+
Type::tuple(TupleType::new(db, &builder.build()))
73067349
}
73077350

73087351
fn infer_list_expression(&mut self, list: &ast::ExprList, tcx: TypeContext<'db>) -> Type<'db> {
@@ -7459,7 +7502,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
74597502

74607503
let inferable = generic_context.inferable_typevars(self.db());
74617504

7462-
// Remove any union elements of that are unrelated to the collection type.
7505+
// Remove any union elements of the annotation that are unrelated to the collection type.
74637506
//
74647507
// For example, we only want the `list[int]` from `annotation: list[int] | None` if
74657508
// `collection_ty` is `list`.
@@ -7499,8 +7542,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
74997542
}
75007543

75017544
let elt_tcxs = match annotated_elt_tys {
7502-
None => Either::Left(iter::repeat(TypeContext::default())),
7503-
Some(tys) => Either::Right(tys.iter().map(|ty| TypeContext::new(Some(*ty)))),
7545+
None => Either::Left(iter::repeat(None)),
7546+
Some(tys) => Either::Right(tys.iter().copied().map(Some)),
75047547
};
75057548

75067549
for elts in elts {
@@ -7529,6 +7572,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
75297572
{
75307573
let Some(elt) = elt else { continue };
75317574

7575+
let elt_tcx = if let ast::Expr::Starred(starred) = elt {
7576+
elt_tcx
7577+
.map(|ty| TypeContext::for_starred_expression(self.db(), ty, starred))
7578+
.unwrap_or_default()
7579+
} else {
7580+
TypeContext::new(elt_tcx)
7581+
};
7582+
75327583
let inferred_elt_ty = infer_elt_expression(self, elt, elt_tcx);
75337584

75347585
// Simplify the inference based on the declared type of the element.
@@ -7542,7 +7593,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
75427593
// unions for large nested list literals, which the constraint solver struggles with.
75437594
let inferred_elt_ty = inferred_elt_ty.promote_literals(self.db(), elt_tcx);
75447595

7545-
builder.infer(Type::TypeVar(elt_ty), inferred_elt_ty).ok()?;
7596+
builder
7597+
.infer(
7598+
Type::TypeVar(elt_ty),
7599+
if elt.is_starred_expr() {
7600+
inferred_elt_ty
7601+
.iterate(self.db())
7602+
.homogeneous_element_type(self.db())
7603+
} else {
7604+
inferred_elt_ty
7605+
},
7606+
)
7607+
.ok()?;
75467608
}
75477609
}
75487610

@@ -8359,25 +8421,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
83598421
}
83608422
}
83618423

8362-
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
8424+
fn infer_starred_expression(
8425+
&mut self,
8426+
starred: &ast::ExprStarred,
8427+
tcx: TypeContext<'db>,
8428+
) -> Type<'db> {
83638429
let ast::ExprStarred {
83648430
range: _,
83658431
node_index: _,
83668432
value,
83678433
ctx: _,
83688434
} = starred;
83698435

8370-
let iterable_type = self.infer_expression(value, TypeContext::default());
8436+
let db = self.db();
8437+
let iterable_type = self.infer_expression(value, tcx);
8438+
83718439
iterable_type
8372-
.try_iterate(self.db())
8373-
.map(|tuple| tuple.homogeneous_element_type(self.db()))
8440+
.try_iterate(db)
8441+
.map(|spec| Type::tuple(TupleType::new(db, &spec)))
83748442
.unwrap_or_else(|err| {
83758443
err.report_diagnostic(&self.context, iterable_type, value.as_ref().into());
8376-
err.fallback_element_type(self.db())
8377-
});
8378-
8379-
// TODO
8380-
todo_type!("starred expression")
8444+
Type::homogeneous_tuple(db, err.fallback_element_type(db))
8445+
})
83818446
}
83828447

83838448
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
@@ -166,9 +166,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
166166
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
167167

168168
// Annotation expressions also get special handling for `*args` and `**kwargs`.
169-
ast::Expr::Starred(starred) => {
170-
TypeAndQualifiers::declared(self.infer_starred_expression(starred))
171-
}
169+
ast::Expr::Starred(starred) => TypeAndQualifiers::declared(
170+
self.infer_starred_expression(starred, TypeContext::default()),
171+
),
172172

173173
ast::Expr::BytesLiteral(bytes) => {
174174
if let Some(builder) = self

0 commit comments

Comments
 (0)