Skip to content

Commit 66f50fb

Browse files
[ty] Add property test generators for variable-length tuples (#18901)
Add property test generators for the new variable-length tuples. This covers homogeneous tuples as well. The property tests did their job! This identified several fixes we needed to make to various type property methods. cf #18600 (comment) --------- Co-authored-by: Alex Waygood <[email protected]>
1 parent 919af96 commit 66f50fb

File tree

7 files changed

+546
-188
lines changed

7 files changed

+546
-188
lines changed

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

Lines changed: 200 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,138 @@ static_assert(is_singleton(None))
9999
static_assert(not is_singleton(tuple[None]))
100100
```
101101

102+
## Tuples containing `Never`
103+
104+
```toml
105+
[environment]
106+
python-version = "3.11"
107+
```
108+
109+
The `Never` type contains no inhabitants, so a tuple type that contains `Never` as a mandatory
110+
element also contains no inhabitants.
111+
112+
```py
113+
from typing import Never
114+
from ty_extensions import static_assert, is_equivalent_to
115+
116+
static_assert(is_equivalent_to(tuple[Never], Never))
117+
static_assert(is_equivalent_to(tuple[int, Never], Never))
118+
static_assert(is_equivalent_to(tuple[Never, *tuple[int, ...]], Never))
119+
```
120+
121+
If the variable-length portion of a tuple is `Never`, then that portion of the tuple must always be
122+
empty. This means that the tuple is not actually variable-length!
123+
124+
```py
125+
from typing import Never
126+
from ty_extensions import static_assert, is_equivalent_to
127+
128+
static_assert(is_equivalent_to(tuple[Never, ...], tuple[()]))
129+
static_assert(is_equivalent_to(tuple[int, *tuple[Never, ...]], tuple[int]))
130+
static_assert(is_equivalent_to(tuple[int, *tuple[Never, ...], int], tuple[int, int]))
131+
static_assert(is_equivalent_to(tuple[*tuple[Never, ...], int], tuple[int]))
132+
```
133+
134+
## Homogeneous non-empty tuples
135+
136+
```toml
137+
[environment]
138+
python-version = "3.11"
139+
```
140+
141+
A homogeneous tuple can contain zero or more elements of a particular type. You can represent a
142+
tuple that can contain _one_ or more elements of that type (or any other number of minimum elements)
143+
using a mixed tuple.
144+
145+
```py
146+
def takes_zero_or_more(t: tuple[int, ...]) -> None: ...
147+
def takes_one_or_more(t: tuple[int, *tuple[int, ...]]) -> None: ...
148+
def takes_two_or_more(t: tuple[int, int, *tuple[int, ...]]) -> None: ...
149+
150+
takes_zero_or_more(())
151+
takes_zero_or_more((1,))
152+
takes_zero_or_more((1, 2))
153+
154+
takes_one_or_more(()) # error: [invalid-argument-type]
155+
takes_one_or_more((1,))
156+
takes_one_or_more((1, 2))
157+
158+
takes_two_or_more(()) # error: [invalid-argument-type]
159+
takes_two_or_more((1,)) # error: [invalid-argument-type]
160+
takes_two_or_more((1, 2))
161+
```
162+
163+
The required elements can also appear in the suffix of the mixed tuple type.
164+
165+
```py
166+
def takes_one_or_more_suffix(t: tuple[*tuple[int, ...], int]) -> None: ...
167+
def takes_two_or_more_suffix(t: tuple[*tuple[int, ...], int, int]) -> None: ...
168+
def takes_two_or_more_mixed(t: tuple[int, *tuple[int, ...], int]) -> None: ...
169+
170+
takes_one_or_more_suffix(()) # error: [invalid-argument-type]
171+
takes_one_or_more_suffix((1,))
172+
takes_one_or_more_suffix((1, 2))
173+
174+
takes_two_or_more_suffix(()) # error: [invalid-argument-type]
175+
takes_two_or_more_suffix((1,)) # error: [invalid-argument-type]
176+
takes_two_or_more_suffix((1, 2))
177+
178+
takes_two_or_more_mixed(()) # error: [invalid-argument-type]
179+
takes_two_or_more_mixed((1,)) # error: [invalid-argument-type]
180+
takes_two_or_more_mixed((1, 2))
181+
```
182+
183+
The tuple types are equivalent regardless of whether the required elements appear in the prefix or
184+
suffix.
185+
186+
```py
187+
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to
188+
189+
static_assert(is_equivalent_to(tuple[int, *tuple[int, ...]], tuple[*tuple[int, ...], int]))
190+
191+
static_assert(is_equivalent_to(tuple[int, int, *tuple[int, ...]], tuple[*tuple[int, ...], int, int]))
192+
static_assert(is_equivalent_to(tuple[int, int, *tuple[int, ...]], tuple[int, *tuple[int, ...], int]))
193+
```
194+
195+
This is true when the prefix/suffix and variable-length types are equivalent, not just identical.
196+
197+
```py
198+
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to
199+
200+
static_assert(is_equivalent_to(tuple[int | str, *tuple[str | int, ...]], tuple[*tuple[str | int, ...], int | str]))
201+
202+
static_assert(
203+
is_equivalent_to(tuple[int | str, str | int, *tuple[str | int, ...]], tuple[*tuple[int | str, ...], str | int, int | str])
204+
)
205+
static_assert(
206+
is_equivalent_to(tuple[int | str, str | int, *tuple[str | int, ...]], tuple[str | int, *tuple[int | str, ...], int | str])
207+
)
208+
```
209+
102210
## Disjointness
103211

104-
A tuple `tuple[P1, P2]` is disjoint from a tuple `tuple[Q1, Q2]` if either `P1` is disjoint from
105-
`Q1` or if `P2` is disjoint from `Q2`:
212+
```toml
213+
[environment]
214+
python-version = "3.11"
215+
```
216+
217+
Two tuples with incompatible minimum lengths are always disjoint, regardless of their element types.
218+
(The lengths are incompatible if the minimum length of one tuple is larger than the maximum length
219+
of the other.)
106220

107221
```py
108222
from ty_extensions import static_assert, is_disjoint_from
223+
224+
static_assert(is_disjoint_from(tuple[()], tuple[int]))
225+
static_assert(not is_disjoint_from(tuple[()], tuple[int, ...]))
226+
static_assert(not is_disjoint_from(tuple[int], tuple[int, ...]))
227+
static_assert(not is_disjoint_from(tuple[str, ...], tuple[int, ...]))
228+
```
229+
230+
A tuple that is required to contain elements `P1, P2` is disjoint from a tuple that is required to
231+
contain elements `Q1, Q2` if either `P1` is disjoint from `Q1` or if `P2` is disjoint from `Q2`.
232+
233+
```py
109234
from typing import final
110235

111236
@final
@@ -124,9 +249,28 @@ static_assert(is_disjoint_from(tuple[F1, F2], tuple[F2, F1]))
124249
static_assert(is_disjoint_from(tuple[F1, N1], tuple[F2, N2]))
125250
static_assert(is_disjoint_from(tuple[N1, F1], tuple[N2, F2]))
126251
static_assert(not is_disjoint_from(tuple[N1, N2], tuple[N2, N1]))
252+
253+
static_assert(is_disjoint_from(tuple[F1, *tuple[int, ...], F2], tuple[F2, *tuple[int, ...], F1]))
254+
static_assert(is_disjoint_from(tuple[F1, *tuple[int, ...], N1], tuple[F2, *tuple[int, ...], N2]))
255+
static_assert(is_disjoint_from(tuple[N1, *tuple[int, ...], F1], tuple[N2, *tuple[int, ...], F2]))
256+
static_assert(not is_disjoint_from(tuple[N1, *tuple[int, ...], N2], tuple[N2, *tuple[int, ...], N1]))
257+
258+
static_assert(not is_disjoint_from(tuple[F1, F2, *tuple[object, ...]], tuple[*tuple[object, ...], F2, F1]))
259+
static_assert(not is_disjoint_from(tuple[F1, N1, *tuple[object, ...]], tuple[*tuple[object, ...], F2, N2]))
260+
static_assert(not is_disjoint_from(tuple[N1, F1, *tuple[object, ...]], tuple[*tuple[object, ...], N2, F2]))
261+
static_assert(not is_disjoint_from(tuple[N1, N2, *tuple[object, ...]], tuple[*tuple[object, ...], N2, N1]))
262+
```
263+
264+
The variable-length portion of a tuple can never cause the tuples to be disjoint, since all
265+
variable-length tuple types contain the empty tuple. (Note that per above, the variable-length
266+
portion of a tuple cannot be `Never`; internally we simplify this to a fixed-length tuple.)
267+
268+
```py
269+
static_assert(not is_disjoint_from(tuple[F1, ...], tuple[F2, ...]))
270+
static_assert(not is_disjoint_from(tuple[N1, ...], tuple[N2, ...]))
127271
```
128272

129-
We currently model tuple types to *not* be disjoint from arbitrary instance types, because we allow
273+
We currently model tuple types to _not_ be disjoint from arbitrary instance types, because we allow
130274
for the possibility of `tuple` to be subclassed
131275

132276
```py
@@ -152,21 +296,71 @@ class CommonSubtypeOfTuples(I1, I2): ...
152296

153297
## Truthiness
154298

155-
The truthiness of the empty tuple is `False`:
299+
```toml
300+
[environment]
301+
python-version = "3.11"
302+
```
303+
304+
The truthiness of the empty tuple is `False`.
156305

157306
```py
158307
from typing_extensions import assert_type, Literal
308+
from ty_extensions import static_assert, is_assignable_to, AlwaysFalsy
159309

160310
assert_type(bool(()), Literal[False])
311+
312+
static_assert(is_assignable_to(tuple[()], AlwaysFalsy))
161313
```
162314

163-
The truthiness of non-empty tuples is always `True`, even if all elements are falsy:
315+
The truthiness of non-empty tuples is always `True`. This is true even if all elements are falsy,
316+
and even if any element is gradual, since the truthiness of a tuple depends only on its length, not
317+
its content.
164318

165319
```py
166-
from typing_extensions import assert_type, Literal
320+
from typing_extensions import assert_type, Any, Literal
321+
from ty_extensions import static_assert, is_assignable_to, AlwaysTruthy
167322

168323
assert_type(bool((False,)), Literal[True])
169324
assert_type(bool((False, False)), Literal[True])
325+
326+
static_assert(is_assignable_to(tuple[Any], AlwaysTruthy))
327+
static_assert(is_assignable_to(tuple[Any, Any], AlwaysTruthy))
328+
static_assert(is_assignable_to(tuple[bool], AlwaysTruthy))
329+
static_assert(is_assignable_to(tuple[bool, bool], AlwaysTruthy))
330+
static_assert(is_assignable_to(tuple[Literal[False]], AlwaysTruthy))
331+
static_assert(is_assignable_to(tuple[Literal[False], Literal[False]], AlwaysTruthy))
332+
```
333+
334+
The truthiness of variable-length tuples is ambiguous, since that type contains both empty and
335+
non-empty tuples.
336+
337+
```py
338+
from typing_extensions import Any, Literal
339+
from ty_extensions import static_assert, is_assignable_to, AlwaysFalsy, AlwaysTruthy
340+
341+
static_assert(not is_assignable_to(tuple[Any, ...], AlwaysFalsy))
342+
static_assert(not is_assignable_to(tuple[Any, ...], AlwaysTruthy))
343+
static_assert(not is_assignable_to(tuple[bool, ...], AlwaysFalsy))
344+
static_assert(not is_assignable_to(tuple[bool, ...], AlwaysTruthy))
345+
static_assert(not is_assignable_to(tuple[Literal[False], ...], AlwaysFalsy))
346+
static_assert(not is_assignable_to(tuple[Literal[False], ...], AlwaysTruthy))
347+
static_assert(not is_assignable_to(tuple[Literal[True], ...], AlwaysFalsy))
348+
static_assert(not is_assignable_to(tuple[Literal[True], ...], AlwaysTruthy))
349+
350+
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], AlwaysTruthy))
351+
static_assert(is_assignable_to(tuple[int, *tuple[bool, ...]], AlwaysTruthy))
352+
static_assert(is_assignable_to(tuple[int, *tuple[Literal[False], ...]], AlwaysTruthy))
353+
static_assert(is_assignable_to(tuple[int, *tuple[Literal[True], ...]], AlwaysTruthy))
354+
355+
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], AlwaysTruthy))
356+
static_assert(is_assignable_to(tuple[*tuple[bool, ...], int], AlwaysTruthy))
357+
static_assert(is_assignable_to(tuple[*tuple[Literal[False], ...], int], AlwaysTruthy))
358+
static_assert(is_assignable_to(tuple[*tuple[Literal[True], ...], int], AlwaysTruthy))
359+
360+
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], AlwaysTruthy))
361+
static_assert(is_assignable_to(tuple[int, *tuple[bool, ...], int], AlwaysTruthy))
362+
static_assert(is_assignable_to(tuple[int, *tuple[Literal[False], ...], int], AlwaysTruthy))
363+
static_assert(is_assignable_to(tuple[int, *tuple[Literal[True], ...], int], AlwaysTruthy))
170364
```
171365

172366
Both of these results are conflicting with the fact that tuples can be subclassed, and that we

crates/ty_python_semantic/src/types.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3467,7 +3467,14 @@ impl<'db> Type<'db> {
34673467
Type::BooleanLiteral(bool) => Truthiness::from(*bool),
34683468
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
34693469
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
3470-
Type::Tuple(tuple) => Truthiness::from(!tuple.tuple(db).is_empty()),
3470+
Type::Tuple(tuple) => match tuple.tuple(db).size_hint() {
3471+
// The tuple type is AlwaysFalse if it contains only the empty tuple
3472+
(_, Some(0)) => Truthiness::AlwaysFalse,
3473+
// The tuple type is AlwaysTrue if its inhabitants must always have length >=1
3474+
(minimum, _) if minimum > 0 => Truthiness::AlwaysTrue,
3475+
// The tuple type is Ambiguous if its inhabitants could be of any length
3476+
_ => Truthiness::Ambiguous,
3477+
},
34713478
};
34723479

34733480
Ok(truthiness)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ impl<'db> Bindings<'db> {
394394
Some("__constraints__") => {
395395
overload.set_return_type(TupleType::from_elements(
396396
db,
397-
typevar.constraints(db).into_iter().flatten(),
397+
typevar.constraints(db).into_iter().flatten().copied(),
398398
));
399399
}
400400
Some("__default__") => {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1155,7 +1155,7 @@ impl<'db> ClassLiteral<'db> {
11551155
}
11561156
} else {
11571157
let name = Type::string_literal(db, self.name(db));
1158-
let bases = TupleType::from_elements(db, self.explicit_bases(db));
1158+
let bases = TupleType::from_elements(db, self.explicit_bases(db).iter().copied());
11591159
let namespace = KnownClass::Dict
11601160
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]);
11611161

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8200,7 +8200,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
82008200
};
82018201

82028202
if let Ok(new_elements) = tuple.py_slice(self.db(), start, stop, step) {
8203-
TupleType::from_elements(self.db(), new_elements)
8203+
TupleType::from_elements(self.db(), new_elements.copied())
82048204
} else {
82058205
report_slice_step_size_zero(&self.context, value_node.into());
82068206
Type::unknown()

crates/ty_python_semantic/src/types/property_tests/type_generation.rs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ pub(crate) enum Ty {
3939
pos: Vec<Ty>,
4040
neg: Vec<Ty>,
4141
},
42-
Tuple(Vec<Ty>),
42+
FixedLengthTuple(Vec<Ty>),
43+
VariableLengthTuple(Vec<Ty>, Box<Ty>, Vec<Ty>),
4344
SubclassOfAny,
4445
SubclassOfBuiltinClass(&'static str),
4546
SubclassOfAbcClass(&'static str),
@@ -159,10 +160,16 @@ impl Ty {
159160
}
160161
builder.build()
161162
}
162-
Ty::Tuple(tys) => {
163+
Ty::FixedLengthTuple(tys) => {
163164
let elements = tys.into_iter().map(|ty| ty.into_type(db));
164165
TupleType::from_elements(db, elements)
165166
}
167+
Ty::VariableLengthTuple(prefix, variable, suffix) => {
168+
let prefix = prefix.into_iter().map(|ty| ty.into_type(db));
169+
let variable = variable.into_type(db);
170+
let suffix = suffix.into_iter().map(|ty| ty.into_type(db));
171+
TupleType::mixed(db, prefix, variable, suffix)
172+
}
166173
Ty::SubclassOfAny => SubclassOfType::subclass_of_any(),
167174
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
168175
db,
@@ -268,27 +275,36 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
268275
if size == 0 {
269276
arbitrary_core_type(g)
270277
} else {
271-
match u32::arbitrary(g) % 5 {
278+
match u32::arbitrary(g) % 6 {
272279
0 => arbitrary_core_type(g),
273280
1 => Ty::Union(
274281
(0..*g.choose(&[2, 3]).unwrap())
275282
.map(|_| arbitrary_type(g, size - 1))
276283
.collect(),
277284
),
278-
2 => Ty::Tuple(
285+
2 => Ty::FixedLengthTuple(
286+
(0..*g.choose(&[0, 1, 2]).unwrap())
287+
.map(|_| arbitrary_type(g, size - 1))
288+
.collect(),
289+
),
290+
3 => Ty::VariableLengthTuple(
291+
(0..*g.choose(&[0, 1, 2]).unwrap())
292+
.map(|_| arbitrary_type(g, size - 1))
293+
.collect(),
294+
Box::new(arbitrary_type(g, size - 1)),
279295
(0..*g.choose(&[0, 1, 2]).unwrap())
280296
.map(|_| arbitrary_type(g, size - 1))
281297
.collect(),
282298
),
283-
3 => Ty::Intersection {
299+
4 => Ty::Intersection {
284300
pos: (0..*g.choose(&[0, 1, 2]).unwrap())
285301
.map(|_| arbitrary_type(g, size - 1))
286302
.collect(),
287303
neg: (0..*g.choose(&[0, 1, 2]).unwrap())
288304
.map(|_| arbitrary_type(g, size - 1))
289305
.collect(),
290306
},
291-
4 => Ty::Callable {
307+
5 => Ty::Callable {
292308
params: match u32::arbitrary(g) % 2 {
293309
0 => CallableParams::GradualForm,
294310
1 => CallableParams::List(arbitrary_parameter_list(g, size)),
@@ -398,11 +414,34 @@ impl Arbitrary for Ty {
398414
1 => Some(elts.into_iter().next().unwrap()),
399415
_ => Some(Ty::Union(elts)),
400416
})),
401-
Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
402-
0 => None,
403-
1 => Some(elts.into_iter().next().unwrap()),
404-
_ => Some(Ty::Tuple(elts)),
405-
})),
417+
Ty::FixedLengthTuple(types) => {
418+
Box::new(types.shrink().filter_map(|elts| match elts.len() {
419+
0 => None,
420+
1 => Some(elts.into_iter().next().unwrap()),
421+
_ => Some(Ty::FixedLengthTuple(elts)),
422+
}))
423+
}
424+
Ty::VariableLengthTuple(prefix, variable, suffix) => {
425+
// We shrink the suffix first, then the prefix, then the variable-length type.
426+
let suffix_shrunk = suffix.shrink().map({
427+
let prefix = prefix.clone();
428+
let variable = variable.clone();
429+
move |suffix| Ty::VariableLengthTuple(prefix.clone(), variable.clone(), suffix)
430+
});
431+
let prefix_shrunk = prefix.shrink().map({
432+
let variable = variable.clone();
433+
let suffix = suffix.clone();
434+
move |prefix| Ty::VariableLengthTuple(prefix, variable.clone(), suffix.clone())
435+
});
436+
let variable_shrunk = variable.shrink().map({
437+
let prefix = prefix.clone();
438+
let suffix = suffix.clone();
439+
move |variable| {
440+
Ty::VariableLengthTuple(prefix.clone(), variable, suffix.clone())
441+
}
442+
});
443+
Box::new(suffix_shrunk.chain(prefix_shrunk).chain(variable_shrunk))
444+
}
406445
Ty::Intersection { pos, neg } => {
407446
// Shrinking on intersections is not exhaustive!
408447
//

0 commit comments

Comments
 (0)