Skip to content

Commit 7ce39e5

Browse files
committed
fold tuple * and + on fixed-length literal tuples
`(1, "a") * 3` now folds to `tuple[Literal[1], Literal["a"], ...×3]` and `(1, 2) + (3, 4)` to `tuple[Literal[1], Literal[2], Literal[3], Literal[4]]`, mirroring `tuple.__mul__` / `tuple.__add__` at runtime instead of widening to `tuple[T, ...]`. non-fixed tuples, non-literal factors, and oversized results fall back to typeshed. a side effect is that `instance-layout-conflict` now fires on the previously-TODO slots case, since `__slots__ += (...)` yields a fixed-length tuple
1 parent cb4dd09 commit 7ce39e5

5 files changed

Lines changed: 129 additions & 12 deletions

File tree

crates/ty_python_semantic/resources/mdtest/assignment/augmented.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ reveal_type(x) # revealed: int | float
1313

1414
x = (1, 2)
1515
x += (3, 4)
16-
reveal_type(x) # revealed: tuple[Literal[1, 2, 3, 4], ...]
16+
reveal_type(x) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
1717
```
1818

1919
## Walrus target

crates/ty_python_semantic/resources/mdtest/bidirectional.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,11 @@ x: tuple[list[Literal[1]]] = (list1(1),)
134134
reveal_type(x) # revealed: tuple[list[Literal[1]]]
135135

136136
x: tuple[list[Literal[1]], ...] = (list1(1),) * 3
137-
reveal_type(x) # revealed: tuple[list[Literal[1]], ...]
137+
reveal_type(x) # revealed: tuple[list[Literal[1]], list[Literal[1]], list[Literal[1]]]
138138

139139
x: tuple[list[Literal[1]], ...] = 3 * ((list1(1),) + (list1(1),))
140-
reveal_type(x) # revealed: tuple[list[Literal[1]], ...]
140+
# revealed: tuple[list[Literal[1]], list[Literal[1]], list[Literal[1]], list[Literal[1]], list[Literal[1]], list[Literal[1]]]
141+
reveal_type(x)
141142

142143
x: set[int | str] = {1, 2} | {3, 4}
143144
reveal_type(x) # revealed: set[int | str]

crates/ty_python_semantic/resources/mdtest/binary/tuples.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
## Concatenation for heterogeneous tuples
44

5+
Concatenating two fixed-length tuples folds into a fixed-length tuple that preserves the exact
6+
element order and count, rather than widening to `tuple[T, ...]`.
7+
58
```py
6-
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1, 2, 3, 4], ...]
7-
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1, 2], ...]
8-
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1, 2], ...]
9+
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
10+
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]]
11+
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]]
912
reveal_type(() + ()) # revealed: tuple[()]
1013

1114
def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
12-
reveal_type(x + y) # revealed: tuple[int | str | None | tuple[int], ...]
13-
reveal_type(y + x) # revealed: tuple[None | tuple[int] | int | str, ...]
15+
reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]]
16+
reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str]
1417
```
1518

1619
## Concatenation for homogeneous tuples
@@ -46,3 +49,38 @@ def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour):
4649
reveal_type(one_two + x + three_four) # revealed: tuple[int, ...]
4750
reveal_type(one_two + y + three_four + x) # revealed: tuple[int | str, ...]
4851
```
52+
53+
## Repetition for heterogeneous tuples
54+
55+
Multiplying a fixed-length tuple by a literal integer folds into a fixed-length tuple whose elements
56+
are repeated, matching the runtime behaviour of `tuple.__mul__`. Repetition is commutative, and a
57+
`bool` factor is treated as `0` or `1`.
58+
59+
```py
60+
reveal_type((1, "a") * 3) # revealed: tuple[Literal[1], Literal["a"], Literal[1], Literal["a"], Literal[1], Literal["a"]]
61+
reveal_type(3 * (1, "a")) # revealed: tuple[Literal[1], Literal["a"], Literal[1], Literal["a"], Literal[1], Literal["a"]]
62+
reveal_type((1, "a") * True) # revealed: tuple[Literal[1], Literal["a"]]
63+
```
64+
65+
A non-positive factor folds to the empty tuple.
66+
67+
```py
68+
reveal_type((1, "a") * 0) # revealed: tuple[()]
69+
reveal_type((1, "a") * -2) # revealed: tuple[()]
70+
```
71+
72+
A non-literal factor, or a factor that would produce a tuple longer than the folding limit, falls
73+
back to typeshed's `tuple.__mul__` (which widens to `tuple[T, ...]`).
74+
75+
```py
76+
def _(n: int):
77+
reveal_type((1, "a") * n) # revealed: tuple[Literal[1, "a"], ...]
78+
reveal_type((0,) * 1000) # revealed: tuple[Literal[0], ...]
79+
```
80+
81+
Homogeneous (variable-length) tuples are also left to `tuple.__mul__`.
82+
83+
```py
84+
def _(x: tuple[int, ...]):
85+
reveal_type(x * 3) # revealed: tuple[int, ...]
86+
```

crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,12 @@ class A:
146146
__slots__ = ()
147147
__slots__ += ("a", "b")
148148

149-
reveal_type(A.__slots__) # revealed: tuple[Literal["a", "b"], ...]
149+
reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
150150

151151
class B:
152152
__slots__ = ("c", "d")
153153

154-
# TODO: ideally this would trigger `[instance-layout-conflict]`
155-
# (but it's also not high-priority)
156-
class C(A, B): ...
154+
class C(A, B): ... # error: [instance-layout-conflict]
157155
```
158156

159157
## Explicitly annotated `__slots__`

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::types::diagnostic::{
1010
DIVISION_BY_ZERO, report_unsupported_augmented_assignment, report_unsupported_binary_operation,
1111
};
1212
use crate::types::set_theoretic::RecursivelyDefined;
13+
use crate::types::tuple::Tuple;
1314
use crate::types::typevar::TypeVarConstraints;
1415
use crate::types::{
1516
DynamicType, InternedConstraintSet, KnownClass, KnownInstanceType, LiteralValueTypeKind,
@@ -1099,6 +1100,33 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
10991100
.ok()
11001101
.map(|binding| binding.return_type(db)),
11011102

1103+
// fold `(a, b) * n` (and `n * (a, b)`) into a fixed-length tuple with the
1104+
// elements repeated `n` times, matching the runtime behaviour of
1105+
// `tuple.__mul__`. without this, typeshed's stub widens the result to
1106+
// `tuple[T, ...]`, discarding the exact element order and count
1107+
(Type::NominalInstance(_), _, ast::Operator::Mult)
1108+
if right_ty.as_int_like_literal().is_some() =>
1109+
{
1110+
self.fold_tuple_repeat(left_ty, right_ty).or_else(|| {
1111+
Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx)
1112+
})
1113+
}
1114+
(_, Type::NominalInstance(_), ast::Operator::Mult)
1115+
if left_ty.as_int_like_literal().is_some() =>
1116+
{
1117+
self.fold_tuple_repeat(right_ty, left_ty).or_else(|| {
1118+
Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx)
1119+
})
1120+
}
1121+
1122+
// fold `(a, b) + (c,)` into `(a, b, c)`. as with `*`, typeshed's `tuple.__add__`
1123+
// otherwise widens the concatenation to `tuple[T, ...]`
1124+
(Type::NominalInstance(_), Type::NominalInstance(_), ast::Operator::Add) => {
1125+
self.fold_tuple_concat(left_ty, right_ty).or_else(|| {
1126+
Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx)
1127+
})
1128+
}
1129+
11021130
// We've handled all of the special cases that we support for literals, so we need to
11031131
// fall back on looking for dunder methods on one of the operand types.
11041132
(
@@ -1161,6 +1189,58 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
11611189
}
11621190
}
11631191

1192+
/// Fold `tuple * n` into a fixed-length tuple whose elements are those of `tuple_ty`
1193+
/// repeated `n` times, where `multiplier` is a literal integer (or `bool`).
1194+
///
1195+
/// Returns `None` — leaving the caller to fall back on typeshed's `tuple.__mul__`, which
1196+
/// widens to `tuple[T, ...]` — when `tuple_ty` is not an exact fixed-length tuple, when
1197+
/// `multiplier` is not a literal integer, or when the repeated tuple would grow beyond
1198+
/// `MAX_LENGTH`. A non-positive multiplier folds to the empty tuple.
1199+
fn fold_tuple_repeat(&self, tuple_ty: Type<'db>, multiplier: Type<'db>) -> Option<Type<'db>> {
1200+
/// Repeating into a longer tuple discards the exact element types, so cap the work.
1201+
const MAX_LENGTH: usize = 512;
1202+
1203+
let db = self.db();
1204+
let factor = multiplier.as_int_like_literal()?;
1205+
let spec = tuple_ty.exact_tuple_instance_spec(db)?;
1206+
let Tuple::Fixed(fixed) = spec.as_ref() else {
1207+
return None;
1208+
};
1209+
1210+
let elements = fixed.all_elements();
1211+
let factor = usize::try_from(factor).unwrap_or(0);
1212+
let new_length = elements.len().checked_mul(factor)?;
1213+
if new_length > MAX_LENGTH {
1214+
return None;
1215+
}
1216+
1217+
let mut repeated = Vec::with_capacity(new_length);
1218+
for _ in 0..factor {
1219+
repeated.extend_from_slice(elements);
1220+
}
1221+
Some(Type::heterogeneous_tuple(db, repeated))
1222+
}
1223+
1224+
/// Fold `left + right` into a single fixed-length tuple concatenating their elements.
1225+
///
1226+
/// Returns `None` — leaving the caller to fall back on typeshed's `tuple.__add__` — unless
1227+
/// both operands are exact fixed-length tuples.
1228+
fn fold_tuple_concat(&self, left_ty: Type<'db>, right_ty: Type<'db>) -> Option<Type<'db>> {
1229+
let db = self.db();
1230+
let left = left_ty.exact_tuple_instance_spec(db)?;
1231+
let right = right_ty.exact_tuple_instance_spec(db)?;
1232+
let (Tuple::Fixed(left), Tuple::Fixed(right)) = (left.as_ref(), right.as_ref()) else {
1233+
return None;
1234+
};
1235+
Some(Type::heterogeneous_tuple(
1236+
db,
1237+
left.all_elements()
1238+
.iter()
1239+
.chain(right.all_elements())
1240+
.copied(),
1241+
))
1242+
}
1243+
11641244
/// Raise a diagnostic if the given type cannot be divided by zero.
11651245
///
11661246
/// Expects the resolved type of the left side of the binary expression.

0 commit comments

Comments
 (0)