Skip to content

Commit a50a993

Browse files
authored
[ty] Make tuple instantiations sound (#18987)
## Summary Ensure that we correctly infer calls such as `tuple((1, 2))`, `tuple(range(42))`, etc. Ensure that we emit errors on invalid calls such as `tuple[int, str]()`. ## Test Plan Mdtests
1 parent 6802c47 commit a50a993

File tree

6 files changed

+104
-18
lines changed

6 files changed

+104
-18
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ bar"""
4141
reveal_type(len(())) # revealed: Literal[0]
4242
reveal_type(len((1,))) # revealed: Literal[1]
4343
reveal_type(len((1, 2))) # revealed: Literal[2]
44-
45-
# TODO: Handle constructor calls
46-
reveal_type(len(tuple())) # revealed: int
44+
reveal_type(len(tuple())) # revealed: Literal[0]
4745

4846
# TODO: Handle star unpacks; Should be: Literal[0]
4947
reveal_type(len((*[],))) # revealed: Literal[1]

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,36 @@ specialization of `tuple` we (TODO: should) check that the values passed in matc
2323
defined in the specialization.
2424

2525
```py
26-
# TODO: revealed: tuple[()]
27-
reveal_type(tuple()) # revealed: tuple[Unknown, ...]
28-
# TODO: revealed: tuple[Literal[1]]
26+
from typing_extensions import Iterable, Never
27+
28+
reveal_type(tuple()) # revealed: tuple[()]
29+
reveal_type(tuple[int]((1,))) # revealed: tuple[int]
30+
reveal_type(().__class__()) # revealed: tuple[()]
31+
reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
32+
33+
def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]):
34+
reveal_type(tuple(x)) # revealed: tuple[int, ...]
35+
reveal_type(tuple(y)) # revealed: tuple[str, ...]
36+
reveal_type(tuple(z)) # revealed: tuple[Unknown, ...]
37+
38+
# This is correct as the only inhabitants of `list[Never]` can be empty lists
39+
reveal_type(tuple(aa)) # revealed: tuple[()]
40+
41+
reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
42+
43+
# TODO: should be `tuple[Literal[1], ...]`
2944
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
45+
46+
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `list[Unknown]`"
3047
reveal_type(tuple[int]([1])) # revealed: tuple[int]
31-
# TODO: error for invalid arguments
32-
reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str]
3348

34-
reveal_type(().__class__()) # revealed: tuple[()]
35-
# TODO: error for invalid arguments
49+
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, str]`, found `tuple[Literal[1]]`"
50+
reveal_type(tuple[int, str]((1,))) # revealed: tuple[int, str]
51+
52+
# error: [missing-argument] "No argument provided for required parameter `iterable`"
3653
reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
37-
# TODO: error for invalid arguments
54+
55+
# error: [missing-argument] "No argument provided for required parameter `iterable`"
3856
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
3957
```
4058

crates/ty_python_semantic/src/types.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,13 @@ impl<'db> Type<'db> {
946946
matches!(self, Type::ClassLiteral(..))
947947
}
948948

949+
pub(crate) const fn into_tuple(self) -> Option<TupleType<'db>> {
950+
match self {
951+
Type::Tuple(tuple_type) => Some(tuple_type),
952+
_ => None,
953+
}
954+
}
955+
949956
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
950957
/// Since a `ClassType` must be specialized, apply the default specialization to any
951958
/// unspecialized generic class literal.
@@ -4237,6 +4244,27 @@ impl<'db> Type<'db> {
42374244
.into()
42384245
}
42394246

4247+
Some(KnownClass::Tuple) => {
4248+
let object = Type::object(db);
4249+
4250+
CallableBinding::from_overloads(
4251+
self,
4252+
[
4253+
Signature::new(Parameters::empty(), Some(TupleType::empty(db))),
4254+
Signature::new(
4255+
Parameters::new([Parameter::positional_only(Some(
4256+
Name::new_static("iterable"),
4257+
))
4258+
.with_annotated_type(
4259+
KnownClass::Iterable.to_specialized_instance(db, [object]),
4260+
)]),
4261+
Some(TupleType::homogeneous(db, object)),
4262+
),
4263+
],
4264+
)
4265+
.into()
4266+
}
4267+
42404268
// Most class literal constructor calls are handled by `try_call_constructor` and
42414269
// not via getting the signature here. This signature can still be used in some
42424270
// cases (e.g. evaluating callable subtyping). TODO improve this definition
@@ -4276,14 +4304,24 @@ impl<'db> Type<'db> {
42764304
.into()
42774305
}
42784306

4279-
Type::GenericAlias(_) => {
4307+
Type::GenericAlias(alias) => {
4308+
let instantiated = Type::instance(db, ClassType::from(alias));
4309+
4310+
let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) {
4311+
let spec = alias.specialization(db).tuple(db);
4312+
let mut parameter =
4313+
Parameter::positional_only(Some(Name::new_static("iterable")))
4314+
.with_annotated_type(instantiated);
4315+
if matches!(spec.size_hint().1, Some(0)) {
4316+
parameter = parameter.with_default_type(TupleType::empty(db));
4317+
}
4318+
Parameters::new([parameter])
4319+
} else {
4320+
Parameters::gradual_form()
4321+
};
42804322
// TODO annotated return type on `__new__` or metaclass `__call__`
42814323
// TODO check call vs signatures of `__new__` and/or `__init__`
4282-
Binding::single(
4283-
self,
4284-
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
4285-
)
4286-
.into()
4324+
Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
42874325
}
42884326

42894327
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,28 @@ impl<'db> Bindings<'db> {
972972
}
973973
}
974974

975+
Some(KnownClass::Tuple) if overload_index == 1 => {
976+
if let [Some(argument)] = overload.parameter_types() {
977+
let overridden_return =
978+
argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| {
979+
// Some awkward special handling is required here because of the fact
980+
// that calling `try_iterate()` on `Never` returns `Never`,
981+
// but `tuple[Never, ...]` eagerly simplifies to `tuple[()]`,
982+
// which will cause us to emit false positives if we index into the tuple
983+
let specialization = if argument.is_never() {
984+
Type::unknown()
985+
} else {
986+
argument.try_iterate(db).expect(
987+
"try_iterate() should not fail on a type \
988+
assignable to `Iterable`",
989+
)
990+
};
991+
TupleType::homogeneous(db, specialization)
992+
});
993+
overload.set_return_type(overridden_return);
994+
}
995+
}
996+
975997
_ => {}
976998
},
977999

crates/ty_python_semantic/src/types/class.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2334,6 +2334,7 @@ pub enum KnownClass {
23342334
NamedTuple,
23352335
NewType,
23362336
SupportsIndex,
2337+
Iterable,
23372338
// Collections
23382339
ChainMap,
23392340
Counter,
@@ -2426,6 +2427,7 @@ impl KnownClass {
24262427
| Self::Float
24272428
| Self::Enum
24282429
| Self::ABCMeta
2430+
| KnownClass::Iterable
24292431
// Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue
24302432
| Self::NamedTuple
24312433
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
@@ -2513,6 +2515,7 @@ impl KnownClass {
25132515
| Self::DefaultDict
25142516
| Self::OrderedDict
25152517
| Self::NewType
2518+
| Self::Iterable
25162519
| Self::BaseExceptionGroup => false,
25172520
}
25182521
}
@@ -2531,7 +2534,7 @@ impl KnownClass {
25312534
/// 2. It's probably more performant.
25322535
const fn is_protocol(self) -> bool {
25332536
match self {
2534-
Self::SupportsIndex => true,
2537+
Self::SupportsIndex | Self::Iterable => true,
25352538

25362539
Self::Any
25372540
| Self::Bool
@@ -2648,6 +2651,7 @@ impl KnownClass {
26482651
Self::Enum => "Enum",
26492652
Self::ABCMeta => "ABCMeta",
26502653
Self::Super => "super",
2654+
Self::Iterable => "Iterable",
26512655
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
26522656
Self::StdlibAlias => "_Alias",
26532657
// This is the name the type of `sys.version_info` has in typeshed,
@@ -2882,6 +2886,7 @@ impl KnownClass {
28822886
| Self::TypeVar
28832887
| Self::NamedTuple
28842888
| Self::StdlibAlias
2889+
| Self::Iterable
28852890
| Self::SupportsIndex => KnownModule::Typing,
28862891
Self::TypeAliasType
28872892
| Self::TypeVarTuple
@@ -2984,6 +2989,7 @@ impl KnownClass {
29842989
| Self::NewType
29852990
| Self::Field
29862991
| Self::KwOnly
2992+
| Self::Iterable
29872993
| Self::NamedTupleFallback => false,
29882994
}
29892995
}
@@ -3052,6 +3058,7 @@ impl KnownClass {
30523058
| Self::NewType
30533059
| Self::Field
30543060
| Self::KwOnly
3061+
| Self::Iterable
30553062
| Self::NamedTupleFallback => false,
30563063
}
30573064
}
@@ -3101,6 +3108,7 @@ impl KnownClass {
31013108
"NewType" => Self::NewType,
31023109
"TypeAliasType" => Self::TypeAliasType,
31033110
"TypeVar" => Self::TypeVar,
3111+
"Iterable" => Self::Iterable,
31043112
"ParamSpec" => Self::ParamSpec,
31053113
"ParamSpecArgs" => Self::ParamSpecArgs,
31063114
"ParamSpecKwargs" => Self::ParamSpecKwargs,
@@ -3197,6 +3205,7 @@ impl KnownClass {
31973205
| Self::ParamSpecKwargs
31983206
| Self::TypeVarTuple
31993207
| Self::NamedTuple
3208+
| Self::Iterable
32003209
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
32013210
}
32023211
}

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5343,6 +5343,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
53435343
| KnownClass::TypeVar
53445344
| KnownClass::NamedTuple
53455345
| KnownClass::TypeAliasType
5346+
| KnownClass::Tuple
53465347
)
53475348
)
53485349
// temporary special-casing for all subclasses of `enum.Enum`

0 commit comments

Comments
 (0)