From a4bcfe95e3aff07fe509194eca61d50f817eafeb Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 21 Aug 2026 14:30:51 -0700 Subject: [PATCH 1/2] [ty] Recover bare TypeVarTuple tuples as gradual tuples --- .../mdtest/generics/legacy/typevartuple.md | 71 ++++++++++++++++++ .../mdtest/generics/pep695/typevartuple.md | 74 +++++++++++++++++++ crates/ty_python_semantic/src/types/infer.rs | 3 + .../infer/builder/annotation_expression.rs | 2 +- .../types/infer/builder/type_expression.rs | 65 +++++++++++----- 5 files changed, 196 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md index 55cf73ace80a0f..76d956b608181f 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md @@ -680,3 +680,74 @@ reveal_type(test(fn0)) # revealed: tuple[()] reveal_type(test(fn1)) # revealed: tuple[str] reveal_type(test(fn2)) # revealed: tuple[str, bytes] ``` + +## Missing unpack + +A legacy type variable tuple must also be unpacked. An invalid tuple annotation recovers to +`tuple[Unknown, ...]`, rather than the single-element `tuple[Unknown]`. This avoids a cascading +assignment error when the value is assigned to a correctly unpacked tuple annotation. + +```py +from typing import Generic, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +# error: [invalid-generic-class] "`TypeVarTuple` must be unpacked with `*` or `Unpack[]` when used as an argument to `Generic`" +class Container(Generic[Ts]): + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + def __init__(self, values: tuple[Ts]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] + self.values: tuple[*Ts] = values +``` + +`typing.Tuple` uses the same recovery as the built-in `tuple`. + +```py +from typing import Tuple + +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def legacy_tuple(values: Tuple[Ts]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] +``` + +## Missing unpack in implicit tuple aliases + +Tuple specializations used to define implicit type aliases recover to `tuple[Unknown, ...]` when a +type variable tuple is not unpacked. This applies to both `tuple` and `typing.Tuple`. + +```py +from typing import Tuple, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +BuiltinAlias = tuple[Ts] # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +LegacyAlias = Tuple[Ts] # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + +reveal_type(BuiltinAlias) # revealed: +reveal_type(LegacyAlias) # revealed: + +def aliases(builtin: BuiltinAlias, legacy: LegacyAlias) -> None: + reveal_type(builtin) # revealed: tuple[Unknown, ...] + reveal_type(legacy) # revealed: tuple[Unknown, ...] +``` + +## Missing unpack in a union-valued tuple element + +A name that may refer to a bare type variable tuple also causes the tuple annotation to recover to +`tuple[Unknown, ...]`, even when the name may refer to a valid element type instead. + +```py +from typing import TypeVarTuple + +Ts = TypeVarTuple("Ts") + +def condition() -> bool: + return True + +Element = Ts if condition() else int + +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def homogeneous_union(values: tuple[Element, ...]) -> tuple[str, ...]: + reveal_type(values) # revealed: tuple[Unknown, ...] + return values +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index 4fae525b2b4ea4..06b427c36de27b 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -1718,6 +1718,8 @@ type Alias[*Ts1, *Ts2] = tuple[*Ts1] | tuple[*Ts2] ### Must always be unpacked +A type variable tuple represents zero or more types, so it cannot be used as a single type. + ```py def invalid[*Ts](x: Ts) -> None: ... # error: [invalid-type-form] def invalid_args[*Ts](*args: Ts) -> None: ... # error: [invalid-type-form] @@ -1726,10 +1728,82 @@ class InvalidTupleElement[*Ts]: # error: [invalid-type-form] "Bare TypeVarTuple `Ts` is not valid in this context in a type expression" values: tuple[Ts] +reveal_type(InvalidTupleElement[int, str]().values) # revealed: tuple[Unknown, ...] + def valid[*Ts](x: tuple[*Ts]) -> tuple[*Ts]: return x ``` +A tuple annotation containing a bare type variable tuple recovers to `tuple[Unknown, ...]`. Treating +the bare pack as one `Unknown` element would incorrectly impose a fixed length, even when other +elements surround it. + +```py +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def mixed[*Ts](values: tuple[int, Ts, str]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] +``` + +### Missing unpack in a homogeneous tuple + +Adding an ellipsis does not make a bare type variable tuple a valid element type. The invalid +specialization recovers to `tuple[Unknown, ...]`. + +```py +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def homogeneous[*Ts](values: tuple[Ts, ...]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] +``` + +### Missing unpack inside another type + +Only the tuple containing the bare pack recovers to `tuple[Unknown, ...]`. An enclosing tuple or +`type[]` annotation keeps its structure. An ordinary tuple with an `Unknown` element keeps its fixed +length. + +```py +from ty_extensions._internal import Unknown + +def nested[*Ts]( + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + values: tuple[tuple[Ts]], + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + cls: type[tuple[Ts]], + fixed: tuple[Unknown], +) -> None: + reveal_type(values) # revealed: tuple[tuple[Unknown, ...]] + reveal_type(cls) # revealed: type[tuple[Unknown, ...]] + reveal_type(fixed) # revealed: tuple[Unknown] +``` + +### Missing unpack in quoted annotations + +Quoting the whole tuple annotation or just the bare type variable tuple does not change the +diagnostic or the fallback type. + +```py +def quoted[*Ts]( + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + whole: "tuple[Ts]", + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + element: tuple["Ts"], +) -> None: + reveal_type(whole) # revealed: tuple[Unknown, ...] + reveal_type(element) # revealed: tuple[Unknown, ...] +``` + +### Other errors alongside a missing unpack + +Recovering from a missing unpack does not prevent us from reporting independent errors in the +remaining tuple elements. + +```py +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +# error: [unresolved-reference] "Name `Missing` used when not defined" +def invalid_sibling[*Ts](values: tuple[Ts, Missing]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] +``` + ### Invalid unpack operand Only tuple types and type variable tuples can be unpacked in a type expression. diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index a9eec2fa1e1c43..5cc8c5b73d7e18 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -86,6 +86,9 @@ bitflags::bitflags! { /// The operand of an `Unpack[...]` expression is neither a tuple nor a `TypeVarTuple`. const INVALID_UNPACK = 1 << 1; + + /// The expression refers to a `TypeVarTuple` without unpacking it. + const INVALID_BARE_TYPE_VAR_TUPLE = 1 << 2; } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index f4c9b0a17c145a..9aef62a253cf88 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -108,7 +108,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_name_or_attribute<'db>( ty: Type<'db>, annotation: &ast::Expr, - builder: &TypeInferenceBuilder<'db, '_>, + builder: &mut TypeInferenceBuilder<'db, '_>, pep_613_policy: PEP613Policy, ) -> AnnotationExpressionInference<'db> { let special_case = match ty { diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 7b043787e483be..a95920f44a3ab8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -22,10 +22,10 @@ use ty_python_core::scope::ScopeKind; use crate::types::{ BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, LiteralValueTypeKind, - Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeContext, TypeFormType, - TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type, - todo_type, + IntersectionType, InvalidTypeExpression, KnownClass, KnownInstanceType, LintDiagnosticGuard, + LiteralValueTypeKind, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, + TypeContext, TypeFormType, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, + UnionType, any_over_type, todo_type, }; use crate::{FxOrderSet, add_inferred_python_version_hint_to_diagnostic}; @@ -99,7 +99,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } pub(super) fn infer_name_or_attribute_type_expression( - &self, + &mut self, ty: Type<'db>, annotation: &ast::Expr, ) -> Type<'db> { @@ -121,6 +121,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.inference_flags(), ) .unwrap_or_else(|error| { + if error.invalid_expressions.iter().any(|invalid| { + matches!(invalid, InvalidTypeExpression::InvalidBareTypeVarTuple(_)) + }) { + self.store_type_expression_flags( + annotation, + TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE, + ); + } error.into_fallback_type(&self.context, annotation, self.inference_flags()) }); self.check_for_unbound_type_variable(annotation, result_ty) @@ -1062,6 +1070,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// This method assumes that a type has already been inferred and stored for the `value` /// of the subscript passed in. + /// + /// Recovers to `tuple[Unknown, ...]` if an element is a `TypeVarTuple` missing an unpack. + /// Recovering that element as `Unknown` would assume a single element where the intended + /// length is unknown. pub(super) fn infer_tuple_type_expression( &mut self, tuple: &ast::ExprSubscript, @@ -1081,9 +1093,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { InferenceFlags::IN_VALID_UNPACK_CONTEXT, previously_in_valid_unpack_context, ); - if self - .type_expression_flags(element) - .contains(TypeExpressionFlags::UNPACK) + let element_flags = self.type_expression_flags(element); + if element_flags.contains(TypeExpressionFlags::UNPACK) && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { let mut diagnostic = @@ -1092,6 +1103,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`...` cannot be used after an unpacked element", ); } + let element_ty = if element_flags + .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE) + { + Type::unknown() + } else { + element_ty + }; let result = TupleType::homogeneous(db, env, element_ty); self.store_expression_type(&tuple.slice, Type::tuple(result)); return result; @@ -1100,6 +1118,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut element_types = TupleSpecBuilder::with_capacity(elements.len()); let mut first_unpacked_variadic_tuple = None; + let mut has_bare_typevartuple = false; for element in elements { if element.is_ellipsis_literal_expr() { @@ -1124,6 +1143,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { InferenceFlags::IN_VALID_UNPACK_CONTEXT, previously_in_valid_unpack_context, ); + has_bare_typevartuple |= self + .type_expression_flags(element) + .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE); // Determine if this element unpacks a tuple: either `*expr` or `Unpack[expr]` let is_unpack = matches!(element, ast::Expr::Starred(_)) || matches!( @@ -1180,7 +1202,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - let ty = TupleType::new(db, env, &element_types.build()); + // Finish inferring every element before recovering, so independent errors are + // still reported. Do not propagate the missing-unpack flag to the tuple itself: + // an enclosing `tuple[tuple[Ts]]` still has exactly one element. + let ty = if has_bare_typevartuple { + TupleType::homogeneous(db, env, Type::unknown()) + } else { + TupleType::new(db, env, &element_types.build()) + }; // Here, we store the type for the inner `int, str` tuple-expression, // while the type for the outer `tuple[int, str]` slice-expression is @@ -1200,7 +1229,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); } self.store_expression_type(single_element, Type::unknown()); - return TupleType::heterogeneous(db, env, std::iter::once(Type::unknown())); + return TupleType::heterogeneous(db, env, [Type::unknown()]); } let previously_in_valid_unpack_context = self .context @@ -1211,6 +1240,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { InferenceFlags::IN_VALID_UNPACK_CONTEXT, previously_in_valid_unpack_context, ); + if self + .type_expression_flags(single_element) + .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE) + { + return TupleType::homogeneous(db, env, Type::unknown()); + } let single_element_is_unpack = matches!(single_element, ast::Expr::Starred(_)) || matches!( single_element, @@ -1226,16 +1261,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else if let Type::TypeVar(typevar) = single_element_ty && typevar.is_typevartuple(self.db()) { - return TupleType::new( - db, - env, - &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(db, env, typevar) - .build(), - ); + return TupleType::unpacked_typevartuple(db, env, typevar); } } - TupleType::heterogeneous(db, env, std::iter::once(single_element_ty)) + TupleType::heterogeneous(db, env, [single_element_ty]) } } } From 432adbf14f8ce94bafe09e71e9643c39c2ccd799 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 21 Aug 2026 15:44:07 -0700 Subject: [PATCH 2/2] [ty] Recover bare TypeVarTuple elements locally --- .../mdtest/generics/legacy/typevartuple.md | 20 +++++----- .../mdtest/generics/pep695/typevartuple.md | 32 +++++++++++---- .../types/infer/builder/type_expression.rs | 39 +++++++------------ 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md index 76d956b608181f..e75b2c78efa288 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md @@ -683,9 +683,10 @@ reveal_type(test(fn2)) # revealed: tuple[str, bytes] ## Missing unpack -A legacy type variable tuple must also be unpacked. An invalid tuple annotation recovers to -`tuple[Unknown, ...]`, rather than the single-element `tuple[Unknown]`. This avoids a cascading -assignment error when the value is assigned to a correctly unpacked tuple annotation. +A legacy type variable tuple must also be unpacked. In a tuple annotation, it recovers as +`*tuple[Unknown, ...]`, so `tuple[Ts]` becomes `tuple[Unknown, ...]`, rather than the single-element +`tuple[Unknown]`. This avoids a cascading assignment error when the value is assigned to a correctly +unpacked tuple annotation. ```py from typing import Generic, TypeVarTuple @@ -712,8 +713,8 @@ def legacy_tuple(values: Tuple[Ts]) -> None: ## Missing unpack in implicit tuple aliases -Tuple specializations used to define implicit type aliases recover to `tuple[Unknown, ...]` when a -type variable tuple is not unpacked. This applies to both `tuple` and `typing.Tuple`. +Tuple specializations used to define implicit type aliases also recover bare type variable tuples as +`*tuple[Unknown, ...]`. This applies to both `tuple` and `typing.Tuple`. ```py from typing import Tuple, TypeVarTuple @@ -733,8 +734,8 @@ def aliases(builtin: BuiltinAlias, legacy: LegacyAlias) -> None: ## Missing unpack in a union-valued tuple element -A name that may refer to a bare type variable tuple also causes the tuple annotation to recover to -`tuple[Unknown, ...]`, even when the name may refer to a valid element type instead. +In a homogeneous tuple annotation, a name that may refer to a bare type variable tuple or a valid +element type preserves the valid alternative and recovers the bare pack to `Unknown`. ```py from typing import TypeVarTuple @@ -747,7 +748,6 @@ def condition() -> bool: Element = Ts if condition() else int # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" -def homogeneous_union(values: tuple[Element, ...]) -> tuple[str, ...]: - reveal_type(values) # revealed: tuple[Unknown, ...] - return values +def homogeneous_union(values: tuple[Element, ...]) -> None: + reveal_type(values) # revealed: tuple[Unknown | int, ...] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index 06b427c36de27b..b1e0f1eca525a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -1734,14 +1734,14 @@ def valid[*Ts](x: tuple[*Ts]) -> tuple[*Ts]: return x ``` -A tuple annotation containing a bare type variable tuple recovers to `tuple[Unknown, ...]`. Treating -the bare pack as one `Unknown` element would incorrectly impose a fixed length, even when other -elements surround it. +A bare type variable tuple in a tuple annotation recovers as `*tuple[Unknown, ...]`, preserving any +fixed elements before and after it. Treating the bare pack as one `Unknown` element would +incorrectly impose a fixed length. ```py # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" def mixed[*Ts](values: tuple[int, Ts, str]) -> None: - reveal_type(values) # revealed: tuple[Unknown, ...] + reveal_type(values) # revealed: tuple[int, *tuple[Unknown, ...], str] ``` ### Missing unpack in a homogeneous tuple @@ -1757,9 +1757,8 @@ def homogeneous[*Ts](values: tuple[Ts, ...]) -> None: ### Missing unpack inside another type -Only the tuple containing the bare pack recovers to `tuple[Unknown, ...]`. An enclosing tuple or -`type[]` annotation keeps its structure. An ordinary tuple with an `Unknown` element keeps its fixed -length. +Recovery only affects the bare pack's position in its tuple. An enclosing tuple or `type[]` +annotation keeps its structure. An ordinary tuple with an `Unknown` element keeps its fixed length. ```py from ty_extensions._internal import Unknown @@ -1801,7 +1800,24 @@ remaining tuple elements. # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" # error: [unresolved-reference] "Name `Missing` used when not defined" def invalid_sibling[*Ts](values: tuple[Ts, Missing]) -> None: - reveal_type(values) # revealed: tuple[Unknown, ...] + reveal_type(values) # revealed: tuple[*tuple[Unknown, ...], Unknown] +``` + +### Missing unpack alongside other variadic elements + +A bare type variable tuple alongside a valid variadic unpack or another bare pack reports only the +missing-unpack errors, without a cascading multiple-unpack error. + +```py +def other_variadic[*Ts]( + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + before: tuple[Ts, *tuple[int, ...]], + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + after: tuple[*tuple[int, ...], Ts], + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + repeated: tuple[Ts, Ts], +) -> None: ... ``` ### Invalid unpack operand diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index a95920f44a3ab8..57270829449d16 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -17,7 +17,7 @@ use crate::types::infer::{InferenceFlags, TypeExpressionFlags}; use crate::types::signatures::{ConcatenateTail, Signature}; use crate::types::special_form::{AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; -use crate::types::tuple::{TupleSpecBuilder, TupleType}; +use crate::types::tuple::{TupleSpec, TupleSpecBuilder, TupleType}; use ty_python_core::scope::ScopeKind; use crate::types::{ @@ -1071,9 +1071,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// This method assumes that a type has already been inferred and stored for the `value` /// of the subscript passed in. /// - /// Recovers to `tuple[Unknown, ...]` if an element is a `TypeVarTuple` missing an unpack. - /// Recovering that element as `Unknown` would assume a single element where the intended - /// length is unknown. + /// Recovers a bare `TypeVarTuple` as `*tuple[Unknown, ...]`, preserving surrounding elements. + /// An enclosing `tuple[tuple[Ts]]` still has exactly one element. pub(super) fn infer_tuple_type_expression( &mut self, tuple: &ast::ExprSubscript, @@ -1093,8 +1092,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { InferenceFlags::IN_VALID_UNPACK_CONTEXT, previously_in_valid_unpack_context, ); - let element_flags = self.type_expression_flags(element); - if element_flags.contains(TypeExpressionFlags::UNPACK) + if self + .type_expression_flags(element) + .contains(TypeExpressionFlags::UNPACK) && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { let mut diagnostic = @@ -1103,13 +1103,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`...` cannot be used after an unpacked element", ); } - let element_ty = if element_flags - .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE) - { - Type::unknown() - } else { - element_ty - }; let result = TupleType::homogeneous(db, env, element_ty); self.store_expression_type(&tuple.slice, Type::tuple(result)); return result; @@ -1118,7 +1111,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut element_types = TupleSpecBuilder::with_capacity(elements.len()); let mut first_unpacked_variadic_tuple = None; - let mut has_bare_typevartuple = false; for element in elements { if element.is_ellipsis_literal_expr() { @@ -1143,9 +1135,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { InferenceFlags::IN_VALID_UNPACK_CONTEXT, previously_in_valid_unpack_context, ); - has_bare_typevartuple |= self - .type_expression_flags(element) - .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE); // Determine if this element unpacks a tuple: either `*expr` or `Unpack[expr]` let is_unpack = matches!(element, ast::Expr::Starred(_)) || matches!( @@ -1197,19 +1186,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { // TODO: emit a diagnostic } + } else if self + .type_expression_flags(element) + .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE) + { + // Do not count recovery as another explicit unpack. + element_types = + element_types.concat(db, env, &TupleSpec::homogeneous(Type::unknown())); } else { element_types.push(element_ty); } } - // Finish inferring every element before recovering, so independent errors are - // still reported. Do not propagate the missing-unpack flag to the tuple itself: - // an enclosing `tuple[tuple[Ts]]` still has exactly one element. - let ty = if has_bare_typevartuple { - TupleType::homogeneous(db, env, Type::unknown()) - } else { - TupleType::new(db, env, &element_types.build()) - }; + let ty = TupleType::new(db, env, &element_types.build()); // Here, we store the type for the inner `int, str` tuple-expression, // while the type for the outer `tuple[int, str]` slice-expression is