From 6a3d6ddca4c93c23e6290173c09a681fd0866291 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 26 Jun 2025 16:57:26 -0400 Subject: [PATCH 01/31] add length to variadic argument --- crates/ty_python_semantic/src/types/call/arguments.rs | 6 +++--- crates/ty_python_semantic/src/types/call/bind.rs | 2 +- crates/ty_python_semantic/src/types/infer.rs | 6 ++++-- crates/ty_python_semantic/src/types/tuple.rs | 4 ++++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index f563eee7bfab3..c1c983d73da9d 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -5,7 +5,7 @@ use itertools::{Either, Itertools}; use crate::Db; use crate::types::KnownClass; -use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use super::Type; @@ -49,8 +49,8 @@ pub(crate) enum Argument<'a> { Synthetic, /// A positional argument. Positional, - /// A starred positional argument (e.g. `*args`). - Variadic, + /// A starred positional argument (e.g. `*args`) containing the specified number of elements. + Variadic(TupleLength), /// A keyword argument (e.g. `a=1`). Keyword(&'a str), /// The double-starred keywords argument (e.g. `**kwargs`). diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 5498635041878..8192101eb3aa8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2160,7 +2160,7 @@ impl<'db> Binding<'db> { Argument::Keyword(name) => { let _ = matcher.match_keyword(argument_index, argument, name); } - Argument::Variadic | Argument::Keywords => { + Argument::Variadic(_) | Argument::Keywords => { // TODO continue; } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index c75446f9a2bc7..99885c519edc5 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -94,7 +94,7 @@ use crate::types::function::{ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::signatures::{CallableSignature, Signature}; -use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, @@ -4542,7 +4542,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|arg_or_keyword| { match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, + ast::Expr::Starred(ast::ExprStarred { .. }) => { + Argument::Variadic(TupleLength::unknown()) + } // TODO diagnostic if after a keyword argument _ => Argument::Positional, }, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 9707dbdac7d09..0eb56aa4cdbfe 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -36,6 +36,10 @@ pub(crate) enum TupleLength { } impl TupleLength { + pub(crate) const fn unknown() -> TupleLength { + TupleLength::Variable(0, 0) + } + /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` /// for a tuple with a variable-length portion.) pub(crate) fn size_hint(self) -> (usize, Option) { From 114a4911c25bdc4ae9774c7e5d54ccabd06f5d54 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 26 Jun 2025 18:30:49 -0400 Subject: [PATCH 02/31] match variadic args --- .../resources/mdtest/call/function.md | 11 ++ .../resources/mdtest/narrow/type_guards.md | 3 +- .../ty_python_semantic/src/types/call/bind.rs | 169 +++++++++++------- crates/ty_python_semantic/src/types/tuple.rs | 4 + 4 files changed, 118 insertions(+), 69 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 847377ae00529..8734da8648b32 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -69,6 +69,17 @@ def _(flag: bool): reveal_type(foo()) # revealed: int ``` +## Splatted arguments + +### Unknown length + +```py +def f(x: int, y: int) -> None: ... + +def _(args: list[int]) -> None: + f(*args) +``` + ## Wrong argument type ### Positional argument, positional-or-keyword parameter diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 69586641c99d0..5d9bcda7c0d39 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -161,8 +161,7 @@ def _(d: Any): if f(): # error: [missing-argument] ... - # TODO: no error, once we support splatted call args - if g(*d): # error: [missing-argument] + if g(*d): ... if f("foo"): # TODO: error: [invalid-type-guard-call] diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 8192101eb3aa8..2861ee30090e3 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3,6 +3,7 @@ //! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a //! union of types, each of which might contain multiple overloads. +use std::borrow::Cow; use std::collections::HashSet; use std::fmt; @@ -27,7 +28,7 @@ use crate::types::function::{ }; use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError}; use crate::types::signatures::{Parameter, ParameterForm, Parameters}; -use crate::types::tuple::TupleType; +use crate::types::tuple::{Tuple, TupleLength, TupleType}; use crate::types::{ BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, @@ -1391,22 +1392,20 @@ impl<'db> CallableBinding<'db> { for overload_index in matching_overload_indexes { let overload = &self.overloads[*overload_index]; - let Some(parameter_index) = overload.argument_parameters[argument_index] else { - // There is no parameter for this argument in this overload. - break; - }; - // TODO: For an unannotated `self` / `cls` parameter, the type should be - // `typing.Self` / `type[typing.Self]` - let current_parameter_type = overload.signature.parameters()[parameter_index] - .annotated_type() - .unwrap_or(Type::unknown()); - if let Some(first_parameter_type) = first_parameter_type { - if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { - participating_parameter_index = Some(parameter_index); - break; + for parameter_index in &overload.argument_parameters[argument_index] { + // TODO: For an unannotated `self` / `cls` parameter, the type should be + // `typing.Self` / `type[typing.Self]` + let current_parameter_type = overload.signature.parameters()[*parameter_index] + .annotated_type() + .unwrap_or(Type::unknown()); + if let Some(first_parameter_type) = first_parameter_type { + if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { + participating_parameter_index = Some(*parameter_index); + break; + } + } else { + first_parameter_type = Some(current_parameter_type); } - } else { - first_parameter_type = Some(current_parameter_type); } } @@ -1434,20 +1433,18 @@ impl<'db> CallableBinding<'db> { let mut current_parameter_types = vec![]; for overload_index in &matching_overload_indexes[..=upto] { let overload = &self.overloads[*overload_index]; - let Some(parameter_index) = overload.argument_parameters[argument_index] else { - // There is no parameter for this argument in this overload. - continue; - }; - if !participating_parameter_indexes.contains(¶meter_index) { - // This parameter doesn't participate in the filtering process. - continue; + for parameter_index in &overload.argument_parameters[argument_index] { + if !participating_parameter_indexes.contains(parameter_index) { + // This parameter doesn't participate in the filtering process. + continue; + } + // TODO: For an unannotated `self` / `cls` parameter, the type should be + // `typing.Self` / `type[typing.Self]` + let parameter_type = overload.signature.parameters()[*parameter_index] + .annotated_type() + .unwrap_or(Type::unknown()); + current_parameter_types.push(parameter_type); } - // TODO: For an unannotated `self` / `cls` parameter, the type should be - // `typing.Self` / `type[typing.Self]` - let parameter_type = overload.signature.parameters()[parameter_index] - .annotated_type() - .unwrap_or(Type::unknown()); - current_parameter_types.push(parameter_type); } if current_parameter_types.is_empty() { continue; @@ -1761,7 +1758,7 @@ struct ArgumentMatcher<'a, 'db> { errors: &'a mut Vec>, /// The parameter that each argument is matched with. - argument_parameters: Vec>, + argument_parameters: Vec>, /// Whether each parameter has been matched with an argument. parameter_matched: Vec, next_positional: usize, @@ -1782,7 +1779,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { argument_forms, conflicting_forms, errors, - argument_parameters: vec![None; arguments.len()], + argument_parameters: vec![smallvec![]; arguments.len()], parameter_matched: vec![false; parameters.len()], next_positional: 0, first_excess_positional: None, @@ -1827,7 +1824,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }); } } - self.argument_parameters[argument_index] = Some(parameter_index); + self.argument_parameters[argument_index].push(parameter_index); self.parameter_matched[parameter_index] = true; } @@ -1881,7 +1878,34 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { Ok(()) } - fn finish(self) -> Box<[Option]> { + fn match_variadic( + &mut self, + argument_index: usize, + argument: Argument<'a>, + length: TupleLength, + ) -> Result<(), ()> { + // We must be able to match up the fixed-length portion of the argument with positional + // parameters, so we pass on any errors that occur. + for _ in 0..length.minimum() { + self.match_positional(argument_index, argument)?; + } + + // If the tuple is variable-length, we assume that it will soak up all remaining positional + // parameters. + if length.is_variable() { + while self + .parameters + .get_positional(self.next_positional) + .is_some() + { + self.match_positional(argument_index, argument)?; + } + } + + Ok(()) + } + + fn finish(self) -> Box<[SmallVec<[usize; 1]>]> { if let Some(first_excess_argument_index) = self.first_excess_positional { self.errors.push(BindingError::TooManyPositionalArguments { first_excess_argument_index: self.get_argument_index(first_excess_argument_index), @@ -1919,7 +1943,7 @@ struct ArgumentTypeChecker<'a, 'db> { signature: &'a Signature<'db>, arguments: &'a CallArguments<'a>, argument_types: &'a [Type<'db>], - argument_parameters: &'a [Option], + argument_parameters: &'a [SmallVec<[usize; 1]>], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, @@ -1933,7 +1957,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { signature: &'a Signature<'db>, arguments: &'a CallArguments<'a>, argument_types: &'a [Type<'db>], - argument_parameters: &'a [Option], + argument_parameters: &'a [SmallVec<[usize; 1]>], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, ) -> Self { @@ -1991,20 +2015,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, _, argument_type) in self.enumerate_argument_types() { - let Some(parameter_index) = self.argument_parameters[argument_index] else { - // There was an error with argument when matching parameters, so don't bother - // type-checking it. - continue; - }; - let parameter = ¶meters[parameter_index]; - let Some(expected_type) = parameter.annotated_type() else { - continue; - }; - if let Err(error) = builder.infer(expected_type, argument_type) { - self.errors.push(BindingError::SpecializationError { - error, - argument_index: adjusted_argument_index, - }); + for parameter_index in &self.argument_parameters[argument_index] { + let parameter = ¶meters[*parameter_index]; + let Some(expected_type) = parameter.annotated_type() else { + continue; + }; + if let Err(error) = builder.infer(expected_type, argument_type) { + self.errors.push(BindingError::SpecializationError { + error, + argument_index: adjusted_argument_index, + }); + } } } self.specialization = self.signature.generic_context.map(|gc| builder.build(gc)); @@ -2020,16 +2041,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn check_argument_type( &mut self, - argument_index: usize, adjusted_argument_index: Option, argument: Argument<'a>, mut argument_type: Type<'db>, + parameter_index: usize, ) { - let Some(parameter_index) = self.argument_parameters[argument_index] else { - // There was an error with argument when matching parameters, so don't bother - // type-checking it. - return; - }; let parameters = self.signature.parameters(); let parameter = ¶meters[parameter_index]; if let Some(mut expected_ty) = parameter.annotated_type() { @@ -2068,12 +2084,30 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { - self.check_argument_type( - argument_index, - adjusted_argument_index, - argument, - argument_type, - ); + let argument_types = match (argument, argument_type) { + (Argument::Variadic(_), Type::Tuple(tuple)) => Cow::Borrowed(tuple.tuple(self.db)), + (Argument::Variadic(_), _) => { + let element_type = argument_type.iterate(self.db); + Cow::Owned(Tuple::homogeneous(element_type)) + } + (_, _) => Cow::Owned(Tuple::homogeneous(argument_type)), + }; + let argument_types = argument_types + .resize( + self.db, + TupleLength::Fixed(self.argument_parameters[argument_index].len()), + ) + .expect("argument type should be consistent with its arity"); + for (argument_type, parameter_index) in + (argument_types.all_elements()).zip(&self.argument_parameters[argument_index]) + { + self.check_argument_type( + adjusted_argument_index, + argument, + *argument_type, + *parameter_index, + ); + } } } @@ -2107,7 +2141,7 @@ pub(crate) struct Binding<'db> { /// The formal parameter that each argument is matched with, in argument source order, or /// `None` if the argument was not matched to any parameter. - argument_parameters: Box<[Option]>, + argument_parameters: Box<[SmallVec<[usize; 1]>]>, /// Bound types for parameters, in parameter source order, or `None` if no argument was matched /// to that parameter. @@ -2160,7 +2194,10 @@ impl<'db> Binding<'db> { Argument::Keyword(name) => { let _ = matcher.match_keyword(argument_index, argument, name); } - Argument::Variadic(_) | Argument::Keywords => { + Argument::Variadic(length) => { + let _ = matcher.match_variadic(argument_index, argument, length); + } + Argument::Keywords => { // TODO continue; } @@ -2229,9 +2266,7 @@ impl<'db> Binding<'db> { argument_types .iter() .zip(&self.argument_parameters) - .filter(move |(_, argument_parameter)| { - argument_parameter.is_some_and(|ap| ap == parameter_index) - }) + .filter(move |(_, argument_parameters)| argument_parameters.contains(¶meter_index)) .map(|(arg_and_type, _)| arg_and_type) } @@ -2303,7 +2338,7 @@ struct BindingSnapshot<'db> { return_ty: Type<'db>, specialization: Option>, inherited_specialization: Option>, - argument_parameters: Box<[Option]>, + argument_parameters: Box<[SmallVec<[usize; 1]>]>, parameter_tys: Box<[Option>]>, errors: Vec>, } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 0eb56aa4cdbfe..54e5c23fd0d6a 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -40,6 +40,10 @@ impl TupleLength { TupleLength::Variable(0, 0) } + pub(crate) fn is_variable(self) -> bool { + matches!(self, TupleLength::Variable(_, _)) + } + /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` /// for a tuple with a variable-length portion.) pub(crate) fn size_hint(self) -> (usize, Option) { From cf39e0dc222cbae6b22aa69e157c6aaa153ba301 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 16:09:07 -0400 Subject: [PATCH 03/31] resize to variable-length --- .../resources/mdtest/call/function.md | 43 +++++++++++++++++-- .../ty_python_semantic/src/types/call/bind.rs | 2 +- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 8734da8648b32..eb85f22e6091f 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -71,13 +71,50 @@ def _(flag: bool): ## Splatted arguments -### Unknown length +### Unknown argument length ```py -def f(x: int, y: int) -> None: ... +def takes_zero() -> None: ... +def takes_one(x: int) -> None: ... +def takes_two(x: int, y: int) -> None: ... +def takes_at_least_zero(*args) -> None: ... +def takes_at_least_one(x: int, *args) -> None: ... +def takes_at_least_two(x: int, y: int, *args) -> None: ... def _(args: list[int]) -> None: - f(*args) + takes_zero(*args) + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + +def _(args: tuple[int, ...]) -> None: + takes_zero(*args) + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) +``` + +### Known argument length + +```py +def takes_zero() -> None: ... +def takes_one(x: int) -> None: ... +def takes_two(x: int, y: int) -> None: ... +def takes_at_least_zero(*args) -> None: ... +def takes_at_least_one(x: int, *args) -> None: ... +def takes_at_least_two(x: int, y: int, *args) -> None: ... + +def _(args: tuple[int]) -> None: + takes_zero(*args) + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) ``` ## Wrong argument type diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 2861ee30090e3..8fc4ead899ace 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2095,7 +2095,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let argument_types = argument_types .resize( self.db, - TupleLength::Fixed(self.argument_parameters[argument_index].len()), + TupleLength::Variable(self.argument_parameters[argument_index].len(), 0), ) .expect("argument type should be consistent with its arity"); for (argument_type, parameter_index) in From c116820f3a0d251a92022e7636c13ca84902ece9 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 17:01:09 -0400 Subject: [PATCH 04/31] infer correct arity for splatted tuples --- .../resources/mdtest/call/function.md | 105 +++++++++++++++++- .../src/types/call/arguments.rs | 6 +- crates/ty_python_semantic/src/types/infer.rs | 61 ++++++---- 3 files changed, 149 insertions(+), 23 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index eb85f22e6091f..265a3ec0fbca8 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -98,7 +98,7 @@ def _(args: tuple[int, ...]) -> None: takes_at_least_two(*args) ``` -### Known argument length +### Fixed-length tuple argument ```py def takes_zero() -> None: ... @@ -109,11 +109,114 @@ def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... def _(args: tuple[int]) -> None: + # error: [too-many-positional-arguments] takes_zero(*args) takes_one(*args) + # error: [missing-argument] takes_two(*args) takes_at_least_zero(*args) takes_at_least_one(*args) + # error: [missing-argument] + takes_at_least_two(*args) + +def _(args: tuple[int, int]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + +def _(args: tuple[int, str]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + # error: [too-many-positional-arguments] + takes_one(*args) + # error: [invalid-argument-type] + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + # error: [invalid-argument-type] + takes_at_least_two(*args) +``` + +### Mixed tuple argument + +```toml +[environment] +python-version = "3.11" +``` + +```py +def takes_zero() -> None: ... +def takes_one(x: int) -> None: ... +def takes_two(x: int, y: int) -> None: ... +def takes_at_least_zero(*args) -> None: ... +def takes_at_least_one(x: int, *args) -> None: ... +def takes_at_least_two(x: int, y: int, *args) -> None: ... + +def _(args: tuple[int, *tuple[int, ...]]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + +def _(args: tuple[int, *tuple[str, ...]]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + takes_one(*args) + # error: [invalid-argument-type] + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + # error: [invalid-argument-type] + takes_at_least_two(*args) + +def _(args: tuple[int, int, *tuple[int, ...]]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + +def _(args: tuple[int, int, *tuple[str, ...]]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + +def _(args: tuple[int, *tuple[int, ...], int]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + +def _(args: tuple[int, *tuple[str, ...], int]) -> None: + # error: [too-many-positional-arguments] + takes_zero(*args) + # error: [too-many-positional-arguments] + takes_one(*args) + # error: [invalid-argument-type] + takes_two(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + # error: [invalid-argument-type] takes_at_least_two(*args) ``` diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index c1c983d73da9d..58ce9d98f7a53 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -37,9 +37,9 @@ impl<'a> CallArguments<'a> { } } -impl<'a> FromIterator> for CallArguments<'a> { - fn from_iter>>(iter: T) -> Self { - Self(iter.into_iter().collect()) +impl<'a> From>> for CallArguments<'a> { + fn from(arguments: Vec>) -> Self { + Self(arguments) } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 99885c519edc5..e2eeb66324e9f 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1862,9 +1862,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_type_parameters(type_params); if let Some(arguments) = class.arguments.as_deref() { - let call_arguments = Self::parse_arguments(arguments); + let (call_arguments, argument_types) = self.parse_arguments(arguments); let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - self.infer_argument_types(arguments, call_arguments, &argument_forms); + self.infer_argument_types(arguments, call_arguments, argument_types, &argument_forms); } } @@ -4536,48 +4536,63 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_expression(expression) } - fn parse_arguments(arguments: &ast::Arguments) -> CallArguments<'_> { - arguments + fn parse_arguments<'a>( + &mut self, + arguments: &'a ast::Arguments, + ) -> (CallArguments<'a>, Vec>>) { + let (arguments, types): (Vec<_>, Vec<_>) = arguments .arguments_source_order() .map(|arg_or_keyword| { match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => { - Argument::Variadic(TupleLength::unknown()) + ast::Expr::Starred(ast::ExprStarred { value, .. }) => { + let ty = self.infer_expression(value); + self.store_expression_type(arg, ty); + let length = match ty { + Type::Tuple(tuple) => tuple.tuple(self.db()).len(), + // TODO: have `Type::try_iterator` return a tuple spec, and use its + // length as this argument's arity + _ => TupleLength::unknown(), + }; + (Argument::Variadic(length), Some(ty)) } // TODO diagnostic if after a keyword argument - _ => Argument::Positional, + _ => (Argument::Positional, None), }, ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { if let Some(arg) = arg { - Argument::Keyword(&arg.id) + (Argument::Keyword(&arg.id), None) } else { // TODO diagnostic if not last - Argument::Keywords + (Argument::Keywords, None) } } } }) - .collect() + .unzip(); + let arguments = CallArguments::from(arguments); + (arguments, types) } fn infer_argument_types<'a>( &mut self, ast_arguments: &ast::Arguments, arguments: CallArguments<'a>, + argument_types: Vec>>, argument_forms: &[Option], ) -> CallArgumentTypes<'a, 'db> { let mut ast_arguments = ast_arguments.arguments_source_order(); CallArgumentTypes::new(arguments, |index, _| { + if let Some(argument_type) = argument_types[index] { + return argument_type; + } let arg_or_keyword = ast_arguments .next() .expect("argument lists should have consistent lengths"); match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { value, .. }) => { - let ty = self.infer_argument_type(value, argument_forms[index]); - self.store_expression_type(arg, ty); - ty + ast::Expr::Starred(ast::ExprStarred { .. }) => { + panic!("should have already inferred a type for splatted argument"); } _ => self.infer_argument_type(arg, argument_forms[index]), }, @@ -5286,7 +5301,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. - let call_arguments = Self::parse_arguments(arguments); + let (call_arguments, argument_types) = self.parse_arguments(arguments); let callable_type = self.infer_expression(func); if let Type::FunctionLiteral(function) = callable_type { @@ -5358,8 +5373,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class)) { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - let call_argument_types = - self.infer_argument_types(arguments, call_arguments, &argument_forms); + let call_argument_types = self.infer_argument_types( + arguments, + call_arguments, + argument_types, + &argument_forms, + ); return callable_type .try_call_constructor(self.db(), call_argument_types) @@ -5373,8 +5392,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bindings = callable_type .bindings(self.db()) .match_parameters(&call_arguments); - let call_argument_types = - self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms); + let call_argument_types = self.infer_argument_types( + arguments, + call_arguments, + argument_types, + &bindings.argument_forms, + ); let mut bindings = match bindings.check_types(self.db(), &call_argument_types) { Ok(bindings) => bindings, From 953af0e24a80d2879d795907f3d3062b666943cb Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 17:03:38 -0400 Subject: [PATCH 05/31] clippy --- crates/ty_python_semantic/src/types/infer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index e2eeb66324e9f..2d125bda22b9e 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1864,7 +1864,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(arguments) = class.arguments.as_deref() { let (call_arguments, argument_types) = self.parse_arguments(arguments); let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - self.infer_argument_types(arguments, call_arguments, argument_types, &argument_forms); + self.infer_argument_types(arguments, call_arguments, &argument_types, &argument_forms); } } @@ -4578,7 +4578,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, ast_arguments: &ast::Arguments, arguments: CallArguments<'a>, - argument_types: Vec>>, + argument_types: &[Option>], argument_forms: &[Option], ) -> CallArgumentTypes<'a, 'db> { let mut ast_arguments = ast_arguments.arguments_source_order(); @@ -5376,7 +5376,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let call_argument_types = self.infer_argument_types( arguments, call_arguments, - argument_types, + &argument_types, &argument_forms, ); @@ -5395,7 +5395,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let call_argument_types = self.infer_argument_types( arguments, call_arguments, - argument_types, + &argument_types, &bindings.argument_forms, ); From 1949dfbae5d32212a1c1b9edf6d8b2733082f163 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 17:04:40 -0400 Subject: [PATCH 06/31] mdlint --- crates/ty_python_semantic/resources/mdtest/call/function.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 265a3ec0fbca8..a578be8428c1c 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -80,7 +80,6 @@ def takes_two(x: int, y: int) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... - def _(args: list[int]) -> None: takes_zero(*args) takes_one(*args) @@ -107,7 +106,6 @@ def takes_two(x: int, y: int) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... - def _(args: tuple[int]) -> None: # error: [too-many-positional-arguments] takes_zero(*args) @@ -156,7 +154,6 @@ def takes_two(x: int, y: int) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... - def _(args: tuple[int, *tuple[int, ...]]) -> None: # error: [too-many-positional-arguments] takes_zero(*args) From 4f4cf2be8709c658185d65a224df115e6b5c8188 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 17:53:08 -0400 Subject: [PATCH 07/31] fix those panics --- crates/ty_python_semantic/src/types/infer.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 2d125bda22b9e..609ea7b2bbce6 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4581,19 +4581,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { argument_types: &[Option>], argument_forms: &[Option], ) -> CallArgumentTypes<'a, 'db> { - let mut ast_arguments = ast_arguments.arguments_source_order(); + let mut iter = ast_arguments.arguments_source_order().zip(argument_types); CallArgumentTypes::new(arguments, |index, _| { - if let Some(argument_type) = argument_types[index] { - return argument_type; - } - let arg_or_keyword = ast_arguments + let (arg_or_keyword, argument_type) = iter .next() .expect("argument lists should have consistent lengths"); match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => { - panic!("should have already inferred a type for splatted argument"); - } + ast::Expr::Starred(ast::ExprStarred { .. }) => argument_type + .expect("should have already inferred a type for splatted argument"), _ => self.infer_argument_type(arg, argument_forms[index]), }, ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => { From 0954aab7fe46d60eb3303b6202c2f3541217ee2c Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 18:07:07 -0400 Subject: [PATCH 08/31] add comments --- .../ty_python_semantic/src/types/call/bind.rs | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 8fc4ead899ace..9700c5bb0e495 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2084,20 +2084,47 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { - let argument_types = match (argument, argument_type) { - (Argument::Variadic(_), Type::Tuple(tuple)) => Cow::Borrowed(tuple.tuple(self.db)), - (Argument::Variadic(_), _) => { + // If the argument isn't splatted, just check its type directly. + let Argument::Variadic(_) = argument else { + for parameter_index in &self.argument_parameters[argument_index] { + self.check_argument_type( + adjusted_argument_index, + argument, + argument_type, + *parameter_index, + ); + } + continue; + }; + + // If the argument is splatted, convert its type into a tuple describing the splatted + // elements. For tuples, we don't have to do anything! For other types, we treat it as + // an iterator, and create a homogeneous tuple of its output type, since we don't know + // how many elements the iterator will produce. + // TODO: update `Type::try_iterate` to return this tuple type for us. + let argument_types = match argument_type { + Type::Tuple(tuple) => Cow::Borrowed(tuple.tuple(self.db)), + _ => { let element_type = argument_type.iterate(self.db); Cow::Owned(Tuple::homogeneous(element_type)) } - (_, _) => Cow::Owned(Tuple::homogeneous(argument_type)), }; + + // Resize the tuple of argument types to line up with the number of parameters this + // argument was matched against. If parameter matching succeeded, then we can guarantee + // that all of the required elements of the splatted tuple will have been matched with + // a parameter. But if parameter matching failed, there might be more required + // elements. That means we can't use TupleLength::Fixed below, because we would + // otherwise get a "too many values" error when parameter matching failed. let argument_types = argument_types .resize( self.db, TupleLength::Variable(self.argument_parameters[argument_index].len(), 0), ) .expect("argument type should be consistent with its arity"); + + // Check the types by zipping through the splatted argument types and their matched + // parameters. for (argument_type, parameter_index) in (argument_types.all_elements()).zip(&self.argument_parameters[argument_index]) { From 910bb1d3f65def3112a165b392d052656ce3db3e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 18:43:18 -0400 Subject: [PATCH 09/31] argument expansion workaround --- .../resources/mdtest/call/function.md | 27 +++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 16 +++++++++++ 2 files changed, 43 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index a578be8428c1c..e42a7f2e9e152 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -217,6 +217,33 @@ def _(args: tuple[int, *tuple[str, ...], int]) -> None: takes_at_least_two(*args) ``` +### Argument expansion regression + +This is a regression that was highlighted by the ecosystem check, which shows that we might need to +rethink how we perform argument expansion during overload resolution. In particular, we might need +to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry +`check_types`. + +The issue is that argument expansion might produce a splatted value with a different arity than what +we originally inferred for the unexpanded value, and that in turn can affect which parameters the +splatted value is matched with. In this example, the ternary operator produces a complex union type, +which we expand when trying to call `range`. Our initial guess at its arity is "zero or more", but +there are individual union elements with more precise arities (such as "exactly two"). `range`, via +overloads and parameter defaults, can take in 1, 2, or 3 parameters. Our initial arity guess causes +us to assign the splatted argument to all three parameters. But when we check the `(0, 100)` union +element during argument expansion, we only have two values to provide for those three parameters. + +For now, we have a workaround that pads out the splatted value with `Unknown` when we encounter this +case, but a proper fix would retry parameter matching for each expanded union element. + +```py +def _(batch_ids=(0, 100)) -> None: + arg = (batch_ids if isinstance(batch_ids, tuple) else (0, batch_ids)) + # revealed: (Unknown & tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & tuple[Unknown, ...]) | tuple[Literal[0], (Unknown & ~tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & ~tuple[Unknown, ...])] + reveal_type(arg) + range(*arg) +``` + ## Wrong argument type ### Positional argument, positional-or-keyword parameter diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 9700c5bb0e495..28cdca4745af7 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2110,6 +2110,22 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } }; + // TODO: When we perform argument expansion during overload resolution, we might need + // to retry both `match_parameters` _and_ `check_types` for each expansion. Currently + // we only retry `check_types`. The issue is that argument expansion might produce a + // splatted value with a different arity than what we originally inferred for the + // unexpanded value, and that in turn can affect which parameters the splatted value is + // matched with. As a workaround, make sure that the splatted tuple contains an + // arbitrary number of `Unknown`s at the end, so that if the expanded value has a + // smaller arity than the unexpanded value, we still have enough values to assign to + // the already matched parameters. + let argument_types = match argument_types.as_ref() { + Tuple::Fixed(_) => { + Cow::Owned(argument_types.concat(self.db, &Tuple::homogeneous(Type::unknown()))) + } + Tuple::Variable(_) => argument_types, + }; + // Resize the tuple of argument types to line up with the number of parameters this // argument was matched against. If parameter matching succeeded, then we can guarantee // that all of the required elements of the splatted tuple will have been matched with From e5bc9358697ba2fe08a9223b4c338c8ae088058d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 18:47:34 -0400 Subject: [PATCH 10/31] mdlint --- crates/ty_python_semantic/resources/mdtest/call/function.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index e42a7f2e9e152..fd4fe4e6c60f3 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -238,7 +238,7 @@ case, but a proper fix would retry parameter matching for each expanded union el ```py def _(batch_ids=(0, 100)) -> None: - arg = (batch_ids if isinstance(batch_ids, tuple) else (0, batch_ids)) + arg = batch_ids if isinstance(batch_ids, tuple) else (0, batch_ids) # revealed: (Unknown & tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & tuple[Unknown, ...]) | tuple[Literal[0], (Unknown & ~tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & ~tuple[Unknown, ...])] reveal_type(arg) range(*arg) From 87f2ff9516dccddc385b4ed136700284ca3d1979 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Jun 2025 18:47:36 -0400 Subject: [PATCH 11/31] refine comment --- crates/ty_python_semantic/src/types/call/bind.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 28cdca4745af7..a6ed6ee5d5b90 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2127,11 +2127,12 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { }; // Resize the tuple of argument types to line up with the number of parameters this - // argument was matched against. If parameter matching succeeded, then we can guarantee - // that all of the required elements of the splatted tuple will have been matched with - // a parameter. But if parameter matching failed, there might be more required - // elements. That means we can't use TupleLength::Fixed below, because we would - // otherwise get a "too many values" error when parameter matching failed. + // argument was matched against. If parameter matching succeeded, then we can (TODO: + // should be able to, see above) guarantee that all of the required elements of the + // splatted tuple will have been matched with a parameter. But if parameter matching + // failed, there might be more required elements. That means we can't use + // TupleLength::Fixed below, because we would otherwise get a "too many values" error + // when parameter matching failed. let argument_types = argument_types .resize( self.db, From e8c476d0f4d63202746f8d923cf9ebec57a5243d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 14 Jul 2025 14:44:45 -0400 Subject: [PATCH 12/31] Combine CallArguments and CallArgumentTypes --- crates/ty_python_semantic/src/types.rs | 41 ++--- crates/ty_python_semantic/src/types/call.rs | 2 +- .../src/types/call/arguments.rs | 166 +++++++----------- .../ty_python_semantic/src/types/call/bind.rs | 81 ++++----- crates/ty_python_semantic/src/types/class.rs | 6 +- .../src/types/diagnostic.rs | 4 +- crates/ty_python_semantic/src/types/infer.rs | 81 ++++----- 7 files changed, 160 insertions(+), 221 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d10aa123e0d4f..88c66449d0c1f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -35,7 +35,7 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::place::{ScopeId, ScopedPlaceId}; use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; -use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallableBinding}; +use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; @@ -2683,7 +2683,7 @@ impl<'db> Type<'db> { if let Place::Type(descr_get, descr_get_boundness) = descr_get { let return_ty = descr_get - .try_call(db, &CallArgumentTypes::positional([self, instance, owner])) + .try_call(db, &CallArguments::positional([self, instance, owner])) .map(|bindings| { if descr_get_boundness == Boundness::Bound { bindings.return_type(db) @@ -3134,9 +3134,10 @@ impl<'db> Type<'db> { self.try_call_dunder( db, "__getattr__", - CallArgumentTypes::positional([Type::StringLiteral( - StringLiteralType::new(db, Box::from(name.as_str())), - )]), + CallArguments::positional([Type::StringLiteral(StringLiteralType::new( + db, + Box::from(name.as_str()), + ))]), ) .map(|outcome| Place::bound(outcome.return_type(db))) // TODO: Handle call errors here. @@ -3155,7 +3156,7 @@ impl<'db> Type<'db> { self.try_call_dunder_with_policy( db, "__getattribute__", - &mut CallArgumentTypes::positional([Type::StringLiteral( + &mut CallArguments::positional([Type::StringLiteral( StringLiteralType::new(db, Box::from(name.as_str())), )]), MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, @@ -3279,7 +3280,7 @@ impl<'db> Type<'db> { // runtime there is a fallback to `__len__`, since `__bool__` takes precedence // and a subclass could add a `__bool__` method. - match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) { + match self.try_call_dunder(db, "__bool__", CallArguments::none()) { Ok(outcome) => { let return_type = outcome.return_type(db); if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { @@ -3478,7 +3479,7 @@ impl<'db> Type<'db> { return usize_len.try_into().ok().map(Type::IntLiteral); } - let return_ty = match self.try_call_dunder(db, "__len__", CallArgumentTypes::none()) { + let return_ty = match self.try_call_dunder(db, "__len__", CallArguments::none()) { Ok(bindings) => bindings.return_type(db), Err(CallDunderError::PossiblyUnbound(bindings)) => bindings.return_type(db), @@ -4398,7 +4399,7 @@ impl<'db> Type<'db> { fn try_call( self, db: &'db dyn Db, - argument_types: &CallArgumentTypes<'_, 'db>, + argument_types: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { self.bindings(db) .match_parameters(argument_types) @@ -4413,7 +4414,7 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, name: &str, - mut argument_types: CallArgumentTypes<'_, 'db>, + mut argument_types: CallArguments<'_, 'db>, ) -> Result, CallDunderError<'db>> { self.try_call_dunder_with_policy( db, @@ -4434,7 +4435,7 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, name: &str, - argument_types: &mut CallArgumentTypes<'_, 'db>, + argument_types: &mut CallArguments<'_, 'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { // Implicit calls to dunder methods never access instance members, so we pass @@ -4496,19 +4497,19 @@ impl<'db> Type<'db> { self.try_call_dunder( db, "__getitem__", - CallArgumentTypes::positional([KnownClass::Int.to_instance(db)]), + CallArguments::positional([KnownClass::Int.to_instance(db)]), ) .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) }; let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { iterator - .try_call_dunder(db, "__next__", CallArgumentTypes::none()) + .try_call_dunder(db, "__next__", CallArguments::none()) .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) }; let dunder_iter_result = self - .try_call_dunder(db, "__iter__", CallArgumentTypes::none()) + .try_call_dunder(db, "__iter__", CallArguments::none()) .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); match dunder_iter_result { @@ -4592,11 +4593,11 @@ impl<'db> Type<'db> { /// pass /// ``` fn try_enter(self, db: &'db dyn Db) -> Result, ContextManagerError<'db>> { - let enter = self.try_call_dunder(db, "__enter__", CallArgumentTypes::none()); + let enter = self.try_call_dunder(db, "__enter__", CallArguments::none()); let exit = self.try_call_dunder( db, "__exit__", - CallArgumentTypes::positional([Type::none(db), Type::none(db), Type::none(db)]), + CallArguments::positional([Type::none(db), Type::none(db), Type::none(db)]), ); // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). @@ -4631,7 +4632,7 @@ impl<'db> Type<'db> { fn try_call_constructor( self, db: &'db dyn Db, - argument_types: CallArgumentTypes<'_, 'db>, + argument_types: CallArguments<'_, 'db>, ) -> Result, ConstructorCallError<'db>> { debug_assert!(matches!( self, @@ -6422,11 +6423,11 @@ impl<'db> ContextManagerError<'db> { Ok(_) | Err(CallDunderError::CallError(..)), Ok(_) | Err(CallDunderError::CallError(..)), ) = ( - context_expression_type.try_call_dunder(db, "__aenter__", CallArgumentTypes::none()), + context_expression_type.try_call_dunder(db, "__aenter__", CallArguments::none()), context_expression_type.try_call_dunder( db, "__aexit__", - CallArgumentTypes::positional([Type::unknown(), Type::unknown(), Type::unknown()]), + CallArguments::positional([Type::unknown(), Type::unknown(), Type::unknown()]), ), ) { diag.info(format_args!( @@ -6489,7 +6490,7 @@ impl<'db> IterationError<'db> { Self::IterCallError(_, dunder_iter_bindings) => dunder_iter_bindings .return_type(db) - .try_call_dunder(db, "__next__", CallArgumentTypes::none()) + .try_call_dunder(db, "__next__", CallArguments::none()) .map(|dunder_next_outcome| Some(dunder_next_outcome.return_type(db))) .unwrap_or_else(|dunder_next_call_error| dunder_next_call_error.return_type(db)), diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index f77cc429a5731..c34489bbbc9fa 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -4,7 +4,7 @@ use crate::Db; mod arguments; pub(crate) mod bind; -pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments}; +pub(super) use arguments::{Argument, CallArguments}; pub(super) use bind::{Binding, Bindings, CallableBinding}; /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 77a4eb3976eca..7bc9e86a2fe37 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::ops::{Deref, DerefMut}; use itertools::{Either, Itertools}; use ruff_python_ast as ast; @@ -10,60 +9,6 @@ use crate::types::tuple::{TupleSpec, TupleType}; use super::Type; -/// Arguments for a single call, in source order. -#[derive(Clone, Debug, Default)] -pub(crate) struct CallArguments<'a>(Vec>); - -impl<'a> CallArguments<'a> { - /// Create `CallArguments` from AST arguments - pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self { - arguments - .arguments_source_order() - .map(|arg_or_keyword| match arg_or_keyword { - ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, - _ => Argument::Positional, - }, - ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { - if let Some(arg) = arg { - Argument::Keyword(&arg.id) - } else { - Argument::Keywords - } - } - }) - .collect() - } - - /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front - /// of this argument list. (If `bound_self` is none, we return the argument list - /// unmodified.) - pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { - if bound_self.is_some() { - let arguments = std::iter::once(Argument::Synthetic) - .chain(self.0.iter().copied()) - .collect(); - Cow::Owned(CallArguments(arguments)) - } else { - Cow::Borrowed(self) - } - } - - pub(crate) fn len(&self) -> usize { - self.0.len() - } - - pub(crate) fn iter(&self) -> impl Iterator> + '_ { - self.0.iter().copied() - } -} - -impl<'a> FromIterator> for CallArguments<'a> { - fn from_iter>>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - #[derive(Clone, Copy, Debug)] pub(crate) enum Argument<'a> { /// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site. @@ -80,12 +25,39 @@ pub(crate) enum Argument<'a> { /// Arguments for a single call, in source order, along with inferred types for each argument. #[derive(Clone, Debug, Default)] -pub(crate) struct CallArgumentTypes<'a, 'db> { - arguments: CallArguments<'a>, +pub(crate) struct CallArguments<'a, 'db> { + arguments: Vec>, types: Vec>, } -impl<'a, 'db> CallArgumentTypes<'a, 'db> { +impl<'a, 'db> CallArguments<'a, 'db> { + fn new(iter: impl IntoIterator, Type<'db>)>) -> Self { + let (arguments, types) = iter.into_iter().unzip(); + Self { arguments, types } + } + + /// Create `CallArguments` from AST arguments + pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self { + Self::new( + arguments + .arguments_source_order() + .map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(arg) => match arg { + ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, + _ => Argument::Positional, + }, + ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { + if let Some(arg) = arg { + Argument::Keyword(&arg.id) + } else { + Argument::Keywords + } + } + }) + .map(|argument| (argument, Type::unknown())), + ) + } + /// Create a [`CallArgumentTypes`] with no arguments. pub(crate) fn none() -> Self { Self::default() @@ -95,22 +67,12 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { /// types. pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { let types: Vec<_> = positional_tys.into_iter().collect(); - let arguments = CallArguments(vec![Argument::Positional; types.len()]); + let arguments = vec![Argument::Positional; types.len()]; Self { arguments, types } } - /// Create a new [`CallArgumentTypes`] to store the inferred types of the arguments in a - /// [`CallArguments`]. Uses the provided callback to infer each argument type. - pub(crate) fn new(arguments: CallArguments<'a>, mut f: F) -> Self - where - F: FnMut(usize, Argument<'a>) -> Type<'db>, - { - let types = arguments - .iter() - .enumerate() - .map(|(idx, argument)| f(idx, argument)) - .collect(); - Self { arguments, types } + pub(crate) fn len(&self) -> usize { + self.arguments.len() } pub(crate) fn types(&self) -> &[Type<'db>] { @@ -122,22 +84,24 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { /// unmodified.) pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { if let Some(bound_self) = bound_self { - let arguments = CallArguments( - std::iter::once(Argument::Synthetic) - .chain(self.arguments.0.iter().copied()) - .collect(), - ); + let arguments = std::iter::once(Argument::Synthetic) + .chain(self.arguments.iter().copied()) + .collect(); let types = std::iter::once(bound_self) .chain(self.types.iter().copied()) .collect(); - Cow::Owned(CallArgumentTypes { arguments, types }) + Cow::Owned(CallArguments { arguments, types }) } else { Cow::Borrowed(self) } } pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { - self.arguments.iter().zip(self.types.iter().copied()) + (self.arguments.iter().copied()).zip(self.types.iter().copied()) + } + + pub(crate) fn iter_mut(&mut self) -> impl Iterator, &mut Type<'db>)> + '_ { + (self.arguments.iter().copied()).zip(self.types.iter_mut()) } /// Returns an iterator on performing [argument type expansion]. @@ -146,17 +110,20 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { /// contains the same arguments, but with one or more of the argument types expanded. /// /// [argument type expansion]: https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion - pub(crate) fn expand(&self, db: &'db dyn Db) -> impl Iterator>>> + '_ { + pub(crate) fn expand( + &self, + db: &'db dyn Db, + ) -> impl Iterator>> + '_ { /// Represents the state of the expansion process. /// /// This is useful to avoid cloning the initial types vector if none of the types can be /// expanded. - enum State<'a, 'db> { - Initial(&'a Vec>), - Expanded(Vec>>), + enum State<'a, 'b, 'db> { + Initial(&'b Vec>), + Expanded(Vec>), } - impl<'db> State<'_, 'db> { + impl<'db> State<'_, '_, 'db> { fn len(&self) -> usize { match self { State::Initial(_) => 1, @@ -164,10 +131,12 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { } } - fn iter(&self) -> impl Iterator>> + '_ { + fn iter(&self) -> impl Iterator]> + '_ { match self { - State::Initial(types) => std::slice::from_ref(*types).iter(), - State::Expanded(expanded) => expanded.iter(), + State::Initial(types) => Either::Left(std::iter::once(types.as_slice())), + State::Expanded(expanded) => { + Either::Right(expanded.iter().map(CallArguments::types)) + } } } } @@ -184,20 +153,23 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { index += 1; }; - let mut expanded_arg_types = Vec::with_capacity(expanded_types.len() * previous.len()); + let mut expanded_arguments = Vec::with_capacity(expanded_types.len() * previous.len()); for pre_expanded_types in previous.iter() { for subtype in &expanded_types { - let mut new_expanded_types = pre_expanded_types.clone(); + let mut new_expanded_types = pre_expanded_types.to_vec(); new_expanded_types[index] = *subtype; - expanded_arg_types.push(new_expanded_types); + expanded_arguments.push(CallArguments { + arguments: self.arguments.clone(), + types: new_expanded_types, + }); } } // Increment the index to move to the next argument type for the next iteration. index += 1; - Some(State::Expanded(expanded_arg_types)) + Some(State::Expanded(expanded_arguments)) }) .skip(1) // Skip the initial state, which has no expanded types. .map(|state| match state { @@ -207,16 +179,10 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { } } -impl<'a> Deref for CallArgumentTypes<'a, '_> { - type Target = CallArguments<'a>; - fn deref(&self) -> &CallArguments<'a> { - &self.arguments - } -} - -impl<'a> DerefMut for CallArgumentTypes<'a, '_> { - fn deref_mut(&mut self) -> &mut CallArguments<'a> { - &mut self.arguments +impl<'a> From>> for CallArguments<'a, '_> { + fn from(arguments: Vec>) -> Self { + let types = vec![Type::unknown(); arguments.len()]; + Self { arguments, types } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 771dec6d892c4..ba0d96a425e21 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -10,10 +10,7 @@ use itertools::Itertools; use ruff_db::parsed::parsed_module; use smallvec::{SmallVec, smallvec}; -use super::{ - Argument, CallArgumentTypes, CallArguments, CallError, CallErrorKind, InferContext, Signature, - Type, -}; +use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type}; use crate::db::Db; use crate::dunder_all::dunder_all_names; use crate::place::{Boundness, Place}; @@ -95,12 +92,11 @@ impl<'db> Bindings<'db> { /// /// The returned bindings tell you which parameter (in each signature) each argument was /// matched against. You can then perform type inference on each argument with extra context - /// about the expected parameter types. (You do this by creating a [`CallArgumentTypes`] object - /// from the `arguments` that you match against.) + /// about the expected parameter types. /// /// Once you have argument types available, you can call [`check_types`][Self::check_types] to /// verify that each argument type is assignable to the corresponding parameter type. - pub(crate) fn match_parameters(mut self, arguments: &CallArguments<'_>) -> Self { + pub(crate) fn match_parameters(mut self, arguments: &CallArguments<'_, 'db>) -> Self { let mut argument_forms = vec![None; arguments.len()]; let mut conflicting_forms = vec![false; arguments.len()]; for binding in &mut self.elements { @@ -123,7 +119,7 @@ impl<'db> Bindings<'db> { pub(crate) fn check_types( mut self, db: &'db dyn Db, - argument_types: &CallArgumentTypes<'_, 'db>, + argument_types: &CallArguments<'_, 'db>, ) -> Result> { for element in &mut self.elements { element.check_types(db, argument_types); @@ -410,7 +406,7 @@ impl<'db> Bindings<'db> { [Some(Type::PropertyInstance(property)), Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call(db, &CallArgumentTypes::positional([*instance])) + .try_call(db, &CallArguments::positional([*instance])) .map(|binding| binding.return_type(db)) { overload.set_return_type(return_ty); @@ -439,7 +435,7 @@ impl<'db> Bindings<'db> { [Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call(db, &CallArgumentTypes::positional([*instance])) + .try_call(db, &CallArguments::positional([*instance])) .map(|binding| binding.return_type(db)) { overload.set_return_type(return_ty); @@ -469,10 +465,9 @@ impl<'db> Bindings<'db> { ] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter.try_call( - db, - &CallArgumentTypes::positional([*instance, *value]), - ) { + if let Err(_call_error) = setter + .try_call(db, &CallArguments::positional([*instance, *value])) + { overload.errors.push(BindingError::InternalCallError( "calling the setter failed", )); @@ -488,10 +483,9 @@ impl<'db> Bindings<'db> { Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { if let [Some(instance), Some(value), ..] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter.try_call( - db, - &CallArgumentTypes::positional([*instance, *value]), - ) { + if let Err(_call_error) = setter + .try_call(db, &CallArguments::positional([*instance, *value])) + { overload.errors.push(BindingError::InternalCallError( "calling the setter failed", )); @@ -1161,7 +1155,7 @@ impl<'db> CallableBinding<'db> { fn match_parameters( &mut self, - arguments: &CallArguments<'_>, + arguments: &CallArguments<'_, 'db>, argument_forms: &mut [Option], conflicting_forms: &mut [bool], ) { @@ -1174,7 +1168,7 @@ impl<'db> CallableBinding<'db> { } } - fn check_types(&mut self, db: &'db dyn Db, argument_types: &CallArgumentTypes<'_, 'db>) { + fn check_types(&mut self, db: &'db dyn Db, argument_types: &CallArguments<'_, 'db>) { // If this callable is a bound method, prepend the self instance onto the arguments list // before checking. let argument_types = argument_types.with_self(self.bound_type); @@ -1186,7 +1180,7 @@ impl<'db> CallableBinding<'db> { // still perform type checking for non-overloaded function to provide better user // experience. if let [overload] = self.overloads.as_mut_slice() { - overload.check_types(db, argument_types.as_ref(), argument_types.types()); + overload.check_types(db, argument_types.as_ref()); } return; } @@ -1194,11 +1188,7 @@ impl<'db> CallableBinding<'db> { // If only one candidate overload remains, it is the winning match. Evaluate it as // a regular (non-overloaded) call. self.matching_overload_index = Some(index); - self.overloads[index].check_types( - db, - argument_types.as_ref(), - argument_types.types(), - ); + self.overloads[index].check_types(db, argument_types.as_ref()); return; } MatchingOverloadIndex::Multiple(indexes) => { @@ -1216,7 +1206,7 @@ impl<'db> CallableBinding<'db> { // Step 2: Evaluate each remaining overload as a regular (non-overloaded) call to determine // whether it is compatible with the supplied argument list. for (_, overload) in self.matching_overloads_mut() { - overload.check_types(db, argument_types.as_ref(), argument_types.types()); + overload.check_types(db, argument_types.as_ref()); } match self.matching_overload_index() { @@ -1232,7 +1222,7 @@ impl<'db> CallableBinding<'db> { // TODO: Step 4 // Step 5 - self.filter_overloads_using_any_or_unknown(db, argument_types.types(), &indexes); + self.filter_overloads_using_any_or_unknown(db, argument_types.as_ref(), &indexes); // We're returning here because this shouldn't lead to argument type expansion. return; @@ -1268,7 +1258,7 @@ impl<'db> CallableBinding<'db> { let pre_evaluation_snapshot = snapshotter.take(self); for (_, overload) in self.matching_overloads_mut() { - overload.check_types(db, argument_types.as_ref(), expanded_argument_types); + overload.check_types(db, expanded_argument_types); } let return_type = match self.matching_overload_index() { @@ -1358,7 +1348,7 @@ impl<'db> CallableBinding<'db> { fn filter_overloads_using_any_or_unknown( &mut self, db: &'db dyn Db, - argument_types: &[Type<'db>], + arguments: &CallArguments<'_, 'db>, matching_overload_indexes: &[usize], ) { // These are the parameter indexes that matches the arguments that participate in the @@ -1372,7 +1362,7 @@ impl<'db> CallableBinding<'db> { // participating parameter indexes. let mut top_materialized_argument_types = vec![]; - for (argument_index, argument_type) in argument_types.iter().enumerate() { + for (argument_index, (_, argument_type)) in arguments.iter().enumerate() { let mut first_parameter_type: Option> = None; let mut participating_parameter_index = None; @@ -1415,8 +1405,8 @@ impl<'db> CallableBinding<'db> { self.overloads[*current_index].mark_as_unmatched_overload(); continue; } - let mut parameter_types = Vec::with_capacity(argument_types.len()); - for argument_index in 0..argument_types.len() { + let mut parameter_types = Vec::with_capacity(arguments.len()); + for argument_index in 0..arguments.len() { // The parameter types at the current argument index. let mut current_parameter_types = vec![]; for overload_index in &matching_overload_indexes[..=upto] { @@ -1904,8 +1894,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, - arguments: &'a CallArguments<'a>, - argument_types: &'a [Type<'db>], + arguments: &'a CallArguments<'a, 'db>, argument_parameters: &'a [Option], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, @@ -1918,8 +1907,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn new( db: &'db dyn Db, signature: &'a Signature<'db>, - arguments: &'a CallArguments<'a>, - argument_types: &'a [Type<'db>], + arguments: &'a CallArguments<'a, 'db>, argument_parameters: &'a [Option], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, @@ -1928,7 +1916,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { db, signature, arguments, - argument_types, argument_parameters, parameter_tys, errors, @@ -1940,9 +1927,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn enumerate_argument_types( &self, ) -> impl Iterator, Argument<'a>, Type<'db>)> + 'a { - let mut iter = (self.arguments.iter()) - .zip(self.argument_types.iter().copied()) - .enumerate(); + let mut iter = self.arguments.iter().enumerate(); let mut num_synthetic_args = 0; std::iter::from_fn(move || { let (argument_index, (argument, argument_type)) = iter.next()?; @@ -2127,7 +2112,7 @@ impl<'db> Binding<'db> { pub(crate) fn match_parameters( &mut self, - arguments: &CallArguments<'_>, + arguments: &CallArguments<'_, 'db>, argument_forms: &mut [Option], conflicting_forms: &mut [bool], ) { @@ -2139,7 +2124,7 @@ impl<'db> Binding<'db> { conflicting_forms, &mut self.errors, ); - for (argument_index, argument) in arguments.iter().enumerate() { + for (argument_index, (argument, _)) in arguments.iter().enumerate() { match argument { Argument::Positional | Argument::Synthetic => { let _ = matcher.match_positional(argument_index, argument); @@ -2158,17 +2143,11 @@ impl<'db> Binding<'db> { self.argument_parameters = matcher.finish(); } - fn check_types( - &mut self, - db: &'db dyn Db, - arguments: &CallArguments<'_>, - argument_types: &[Type<'db>], - ) { + fn check_types(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { let mut checker = ArgumentTypeChecker::new( db, &self.signature, arguments, - argument_types, &self.argument_parameters, &mut self.parameter_tys, &mut self.errors, @@ -2210,7 +2189,7 @@ impl<'db> Binding<'db> { pub(crate) fn arguments_for_parameter<'a>( &'a self, - argument_types: &'a CallArgumentTypes<'a, 'db>, + argument_types: &'a CallArguments<'a, 'db>, parameter_index: usize, ) -> impl Iterator, Type<'db>)> + 'a { argument_types diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 40a8b92ca9157..93914dd90d175 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -38,7 +38,7 @@ use crate::{ place_table, semantic_index, use_def_map, }, types::{ - CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType, + CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType, definition_expression_type, }, }; @@ -1201,7 +1201,7 @@ impl<'db> ClassLiteral<'db> { .to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]); // TODO: Other keyword arguments? - let arguments = CallArgumentTypes::positional([name, bases, namespace]); + let arguments = CallArguments::positional([name, bases, namespace]); let return_ty_result = match metaclass.try_call(db, &arguments) { Ok(bindings) => Ok(bindings.return_type(db)), @@ -3303,7 +3303,7 @@ impl KnownClass { context: &InferContext<'db, '_>, index: &SemanticIndex<'db>, overload_binding: &Binding<'db>, - call_argument_types: &CallArgumentTypes<'_, 'db>, + call_argument_types: &CallArguments<'_, 'db>, call_expression: &ast::ExprCall, ) -> Option> { let db = context.db(); diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index e3cf81f75fb45..8ea50d48d085e 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -2,7 +2,7 @@ use super::call::CallErrorKind; use super::context::InferContext; use super::mro::DuplicateBaseError; use super::{ - CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass, + CallArguments, CallDunderError, ClassBase, ClassLiteral, KnownClass, add_inferred_python_version_hint_to_diagnostic, }; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; @@ -2325,7 +2325,7 @@ pub(crate) fn report_invalid_or_unsupported_base( match base_type.try_call_dunder( db, "__mro_entries__", - CallArgumentTypes::positional([tuple_of_types]), + CallArguments::positional([tuple_of_types]), ) { Ok(ret) => { if ret.return_type(db).is_assignable_to(db, tuple_of_types) { diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 62077774ec40e..0651e4c90ae55 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -85,7 +85,7 @@ use crate::semantic_index::place::{ use crate::semantic_index::{ ApplicableConstraints, EagerSnapshotResult, SemanticIndex, place_table, semantic_index, }; -use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallArguments, CallError}; +use crate::types::call::{Binding, Bindings, CallArguments, CallError}; use crate::types::class::{CodeGeneratorKind, MetaclassErrorKind, SliceLiteral}; use crate::types::diagnostic::{ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, @@ -1966,9 +1966,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_type_parameters(type_params); if let Some(arguments) = class.arguments.as_deref() { - let call_arguments = CallArguments::from_arguments(arguments); + let mut call_arguments = CallArguments::from_arguments(arguments); let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - self.infer_argument_types(arguments, call_arguments, &argument_forms); + self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); } } @@ -2375,7 +2375,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { inferred_ty = match decorator_ty - .try_call(self.db(), &CallArgumentTypes::positional([inferred_ty])) + .try_call(self.db(), &CallArguments::positional([inferred_ty])) .map(|bindings| bindings.return_type(self.db())) { Ok(return_ty) => return_ty, @@ -3461,7 +3461,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy( db, "__setattr__", - &mut CallArgumentTypes::positional([ + &mut CallArguments::positional([ Type::StringLiteral(StringLiteralType::new(db, Box::from(attribute))), value_ty, ]), @@ -3549,7 +3549,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let successful_call = meta_dunder_set .try_call( db, - &CallArgumentTypes::positional([ + &CallArguments::positional([ meta_attr_ty, object_ty, value_ty, @@ -3675,11 +3675,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let successful_call = meta_dunder_set .try_call( db, - &CallArgumentTypes::positional([ - meta_attr_ty, - object_ty, - value_ty, - ]), + &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), ) .is_ok(); @@ -4100,7 +4096,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let call = target_type.try_call_dunder( db, op.in_place_dunder(), - CallArgumentTypes::positional([value_type]), + CallArguments::positional([value_type]), ); match call { @@ -4741,28 +4737,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_argument_types<'a>( &mut self, ast_arguments: &ast::Arguments, - arguments: CallArguments<'a>, + arguments: &mut CallArguments<'a, 'db>, argument_forms: &[Option], - ) -> CallArgumentTypes<'a, 'db> { - let mut ast_arguments = ast_arguments.arguments_source_order(); - CallArgumentTypes::new(arguments, |index, _| { - let arg_or_keyword = ast_arguments - .next() - .expect("argument lists should have consistent lengths"); - match arg_or_keyword { + ) { + let iter = (arguments.iter_mut()) + .zip(argument_forms.iter().copied()) + .zip(ast_arguments.arguments_source_order()); + for (((_, argument_type), form), arg_or_keyword) in iter { + *argument_type = match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { ast::Expr::Starred(ast::ExprStarred { value, .. }) => { - let ty = self.infer_argument_type(value, argument_forms[index]); + let ty = self.infer_argument_type(value, form); self.store_expression_type(arg, ty); ty } - _ => self.infer_argument_type(arg, argument_forms[index]), + _ => self.infer_argument_type(arg, form), }, ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => { - self.infer_argument_type(value, argument_forms[index]) + self.infer_argument_type(value, form) } - } - }) + }; + } } fn infer_argument_type( @@ -5451,7 +5446,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. - let call_arguments = CallArguments::from_arguments(arguments); + let mut call_arguments = CallArguments::from_arguments(arguments); let callable_type = self.infer_maybe_standalone_expression(func); @@ -5524,11 +5519,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class)) { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - let call_argument_types = - self.infer_argument_types(arguments, call_arguments, &argument_forms); + self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); return callable_type - .try_call_constructor(self.db(), call_argument_types) + .try_call_constructor(self.db(), call_arguments) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, callable_type, call_expression.into()); err.return_type() @@ -5539,10 +5533,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bindings = callable_type .bindings(self.db()) .match_parameters(&call_arguments); - let call_argument_types = - self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms); + self.infer_argument_types(arguments, &mut call_arguments, &bindings.argument_forms); - let mut bindings = match bindings.check_types(self.db(), &call_argument_types) { + let mut bindings = match bindings.check_types(self.db(), &call_arguments) { Ok(bindings) => bindings, Err(CallError(_, bindings)) => { bindings.report_diagnostics(&self.context, call_expression.into()); @@ -5575,7 +5568,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &self.context, self.index, overload, - &call_argument_types, + &call_arguments, call_expression, ); if let Some(overridden_return) = overridden_return { @@ -6400,7 +6393,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match operand_type.try_call_dunder( self.db(), unary_dunder_method, - CallArgumentTypes::none(), + CallArguments::none(), ) { Ok(outcome) => outcome.return_type(self.db()), Err(e) => { @@ -6772,7 +6765,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .try_call_dunder( self.db(), reflected_dunder, - CallArgumentTypes::positional([left_ty]), + CallArguments::positional([left_ty]), ) .map(|outcome| outcome.return_type(self.db())) .or_else(|_| { @@ -6780,7 +6773,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .try_call_dunder( self.db(), op.dunder(), - CallArgumentTypes::positional([right_ty]), + CallArguments::positional([right_ty]), ) .map(|outcome| outcome.return_type(self.db())) }) @@ -6792,7 +6785,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .try_call_dunder( self.db(), op.dunder(), - CallArgumentTypes::positional([right_ty]), + CallArguments::positional([right_ty]), ) .map(|outcome| outcome.return_type(self.db())) .ok(); @@ -6805,7 +6798,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .try_call_dunder( self.db(), op.reflected_dunder(), - CallArgumentTypes::positional([left_ty]), + CallArguments::positional([left_ty]), ) .map(|outcome| outcome.return_type(self.db())) .ok() @@ -7537,7 +7530,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // The following resource has details about the rich comparison algorithm: // https://snarky.ca/unravelling-rich-comparison-operators/ let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| { - left.try_call_dunder(db, op.dunder(), CallArgumentTypes::positional([right])) + left.try_call_dunder(db, op.dunder(), CallArguments::positional([right])) .map(|outcome| outcome.return_type(db)) .ok() }; @@ -7583,7 +7576,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Place::Type(contains_dunder, Boundness::Bound) => { // If `__contains__` is available, it is used directly for the membership test. contains_dunder - .try_call(db, &CallArgumentTypes::positional([right, left])) + .try_call(db, &CallArguments::positional([right, left])) .map(|bindings| bindings.return_type(db)) .ok() } @@ -7806,7 +7799,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let slice_node = subscript.slice.as_ref(); let call_argument_types = match slice_node { ast::Expr::Tuple(tuple) => { - let arguments = CallArgumentTypes::positional( + let arguments = CallArguments::positional( tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), ); self.store_expression_type( @@ -7815,7 +7808,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); arguments } - _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), + _ => CallArguments::positional([self.infer_type_expression(slice_node)]), }; let binding = Binding::single(value_ty, generic_context.signature(self.db())); let bindings = match Bindings::from(binding) @@ -8065,7 +8058,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match value_ty.try_call_dunder( self.db(), "__getitem__", - CallArgumentTypes::positional([slice_ty]), + CallArguments::positional([slice_ty]), ) { Ok(outcome) => return outcome.return_type(self.db()), Err(err @ CallDunderError::PossiblyUnbound { .. }) => { @@ -8131,7 +8124,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match ty.try_call( self.db(), - &CallArgumentTypes::positional([value_ty, slice_ty]), + &CallArguments::positional([value_ty, slice_ty]), ) { Ok(bindings) => return bindings.return_type(self.db()), Err(CallError(_, bindings)) => { From 3a5713722297bee893084ffe9dd9af77cccd05b3 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 14 Jul 2025 14:59:11 -0400 Subject: [PATCH 13/31] fix docs --- crates/ty_python_semantic/src/types/call/arguments.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 7bc9e86a2fe37..21019cb185fb0 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -58,13 +58,12 @@ impl<'a, 'db> CallArguments<'a, 'db> { ) } - /// Create a [`CallArgumentTypes`] with no arguments. + /// Create a [`CallArguments`] with no arguments. pub(crate) fn none() -> Self { Self::default() } - /// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument - /// types. + /// Create a [`CallArguments`] from an iterator over non-variadic positional argument types. pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { let types: Vec<_> = positional_tys.into_iter().collect(); let arguments = vec![Argument::Positional; types.len()]; From 8929733d7da3bd577754b86885815513d7fcb8a4 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 14 Jul 2025 16:54:21 -0400 Subject: [PATCH 14/31] wrap in option --- .../src/types/call/arguments.rs | 32 +++++++++++-------- .../ty_python_semantic/src/types/call/bind.rs | 7 ++-- crates/ty_python_semantic/src/types/class.rs | 2 +- crates/ty_python_semantic/src/types/infer.rs | 10 ++++-- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 21019cb185fb0..c4251133c46c6 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -27,11 +27,11 @@ pub(crate) enum Argument<'a> { #[derive(Clone, Debug, Default)] pub(crate) struct CallArguments<'a, 'db> { arguments: Vec>, - types: Vec>, + types: Vec>>, } impl<'a, 'db> CallArguments<'a, 'db> { - fn new(iter: impl IntoIterator, Type<'db>)>) -> Self { + fn new(iter: impl IntoIterator, Option>)>) -> Self { let (arguments, types) = iter.into_iter().unzip(); Self { arguments, types } } @@ -54,7 +54,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } }) - .map(|argument| (argument, Type::unknown())), + .map(|argument| (argument, None)), ) } @@ -65,7 +65,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// Create a [`CallArguments`] from an iterator over non-variadic positional argument types. pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { - let types: Vec<_> = positional_tys.into_iter().collect(); + let types: Vec<_> = positional_tys.into_iter().map(Some).collect(); let arguments = vec![Argument::Positional; types.len()]; Self { arguments, types } } @@ -74,7 +74,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { self.arguments.len() } - pub(crate) fn types(&self) -> &[Type<'db>] { + pub(crate) fn types(&self) -> &[Option>] { &self.types } @@ -82,7 +82,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// of this argument list. (If `bound_self` is none, we return the argument list /// unmodified.) pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { - if let Some(bound_self) = bound_self { + if bound_self.is_some() { let arguments = std::iter::once(Argument::Synthetic) .chain(self.arguments.iter().copied()) .collect(); @@ -95,11 +95,13 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } - pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { + pub(crate) fn iter(&self) -> impl Iterator, Option>)> + '_ { (self.arguments.iter().copied()).zip(self.types.iter().copied()) } - pub(crate) fn iter_mut(&mut self) -> impl Iterator, &mut Type<'db>)> + '_ { + pub(crate) fn iter_mut( + &mut self, + ) -> impl Iterator, &mut Option>)> + '_ { (self.arguments.iter().copied()).zip(self.types.iter_mut()) } @@ -118,7 +120,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// This is useful to avoid cloning the initial types vector if none of the types can be /// expanded. enum State<'a, 'b, 'db> { - Initial(&'b Vec>), + Initial(&'b Vec>>), Expanded(Vec>), } @@ -130,7 +132,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } - fn iter(&self) -> impl Iterator]> + '_ { + fn iter(&self) -> impl Iterator>]> + '_ { match self { State::Initial(types) => Either::Left(std::iter::once(types.as_slice())), State::Expanded(expanded) => { @@ -146,8 +148,10 @@ impl<'a, 'db> CallArguments<'a, 'db> { // Find the next type that can be expanded. let expanded_types = loop { let arg_type = self.types.get(index)?; - if let Some(expanded_types) = expand_type(db, *arg_type) { - break expanded_types; + if let Some(arg_type) = arg_type { + if let Some(expanded_types) = expand_type(db, *arg_type) { + break expanded_types; + } } index += 1; }; @@ -157,7 +161,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { for pre_expanded_types in previous.iter() { for subtype in &expanded_types { let mut new_expanded_types = pre_expanded_types.to_vec(); - new_expanded_types[index] = *subtype; + new_expanded_types[index] = Some(*subtype); expanded_arguments.push(CallArguments { arguments: self.arguments.clone(), types: new_expanded_types, @@ -180,7 +184,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { impl<'a> From>> for CallArguments<'a, '_> { fn from(arguments: Vec>) -> Self { - let types = vec![Type::unknown(); arguments.len()]; + let types = vec![None; arguments.len()]; Self { arguments, types } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index ba0d96a425e21..9e16c88f92245 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1363,6 +1363,7 @@ impl<'db> CallableBinding<'db> { let mut top_materialized_argument_types = vec![]; for (argument_index, (_, argument_type)) in arguments.iter().enumerate() { + let argument_type = argument_type.unwrap_or_else(Type::unknown); let mut first_parameter_type: Option> = None; let mut participating_parameter_index = None; @@ -1926,7 +1927,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn enumerate_argument_types( &self, - ) -> impl Iterator, Argument<'a>, Type<'db>)> + 'a { + ) -> impl Iterator, Argument<'a>, Option>)> + 'a { let mut iter = self.arguments.iter().enumerate(); let mut num_synthetic_args = 0; std::iter::from_fn(move || { @@ -1972,6 +1973,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let Some(expected_type) = parameter.annotated_type() else { continue; }; + let argument_type = argument_type.unwrap_or_else(Type::unknown); if let Err(error) = builder.infer(expected_type, argument_type) { self.errors.push(BindingError::SpecializationError { error, @@ -2040,6 +2042,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { + let argument_type = argument_type.unwrap_or_else(Type::unknown); self.check_argument_type( argument_index, adjusted_argument_index, @@ -2191,7 +2194,7 @@ impl<'db> Binding<'db> { &'a self, argument_types: &'a CallArguments<'a, 'db>, parameter_index: usize, - ) -> impl Iterator, Type<'db>)> + 'a { + ) -> impl Iterator, Option>)> + 'a { argument_types .iter() .zip(&self.argument_parameters) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 93914dd90d175..14642445d61ae 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3485,7 +3485,7 @@ impl KnownClass { db, overload_binding .arguments_for_parameter(call_argument_types, 1) - .map(|(_, ty)| ty) + .map(|(_, ty)| ty.unwrap_or_else(Type::unknown)) .collect::>(), ); Some(TypeVarBoundOrConstraints::Constraints(elements)) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 0651e4c90ae55..436375f492c05 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4744,7 +4744,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .zip(argument_forms.iter().copied()) .zip(ast_arguments.arguments_source_order()); for (((_, argument_type), form), arg_or_keyword) in iter { - *argument_type = match arg_or_keyword { + let ty = match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { ast::Expr::Starred(ast::ExprStarred { value, .. }) => { let ty = self.infer_argument_type(value, form); @@ -4757,6 +4757,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_argument_type(value, form) } }; + *argument_type = Some(ty); } } @@ -7804,7 +7805,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); self.store_expression_type( slice_node, - TupleType::from_elements(self.db(), arguments.iter().map(|(_, ty)| ty)), + TupleType::from_elements( + self.db(), + arguments + .iter() + .map(|(_, ty)| ty.unwrap_or_else(Type::unknown)), + ), ); arguments } From 988479d7359a82418906d226280466d75ef27ec0 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 14 Jul 2025 17:05:25 -0400 Subject: [PATCH 15/31] move around a bit --- crates/ty_python_semantic/src/types/call/bind.rs | 12 ++++++------ crates/ty_python_semantic/src/types/class.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 9e16c88f92245..b79e98d73afa5 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1927,7 +1927,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn enumerate_argument_types( &self, - ) -> impl Iterator, Argument<'a>, Option>)> + 'a { + ) -> impl Iterator, Argument<'a>, Type<'db>)> + 'a { let mut iter = self.arguments.iter().enumerate(); let mut num_synthetic_args = 0; std::iter::from_fn(move || { @@ -1947,7 +1947,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { argument_index, adjusted_argument_index, argument, - argument_type, + argument_type.unwrap_or_else(Type::unknown), )) }) } @@ -1973,7 +1973,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let Some(expected_type) = parameter.annotated_type() else { continue; }; - let argument_type = argument_type.unwrap_or_else(Type::unknown); if let Err(error) = builder.infer(expected_type, argument_type) { self.errors.push(BindingError::SpecializationError { error, @@ -2042,7 +2041,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { - let argument_type = argument_type.unwrap_or_else(Type::unknown); self.check_argument_type( argument_index, adjusted_argument_index, @@ -2194,14 +2192,16 @@ impl<'db> Binding<'db> { &'a self, argument_types: &'a CallArguments<'a, 'db>, parameter_index: usize, - ) -> impl Iterator, Option>)> + 'a { + ) -> impl Iterator, Type<'db>)> + 'a { argument_types .iter() .zip(&self.argument_parameters) .filter(move |(_, argument_parameter)| { argument_parameter.is_some_and(|ap| ap == parameter_index) }) - .map(|(arg_and_type, _)| arg_and_type) + .map(|((argument, argument_type), _)| { + (argument, argument_type.unwrap_or_else(Type::unknown)) + }) } /// Mark this overload binding as an unmatched overload. diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 14642445d61ae..93914dd90d175 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3485,7 +3485,7 @@ impl KnownClass { db, overload_binding .arguments_for_parameter(call_argument_types, 1) - .map(|(_, ty)| ty.unwrap_or_else(Type::unknown)) + .map(|(_, ty)| ty) .collect::>(), ); Some(TypeVarBoundOrConstraints::Constraints(elements)) From 06f75c4459f4d2b42eeaa45721c2ce46d35a0468 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 14 Jul 2025 17:21:50 -0400 Subject: [PATCH 16/31] fix tests --- crates/ty_ide/src/signature_help.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index d3038e3fb18ee..9d49c743bb056 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details( details .argument_to_parameter_mapping .get(current_arg_index) - .and_then(|¶m_index| param_index) + .and_then(|param_index| param_index.first().copied()) .or({ // If we can't find a mapping for this argument, but we have a current // argument index, use that as the active parameter if it's within bounds. @@ -246,7 +246,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails] details .argument_to_parameter_mapping .iter() - .all(Option::is_some) + .all(|mapping| !mapping.is_empty()) }); if let Some(index) = perfect_match { @@ -261,7 +261,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails] details .argument_to_parameter_mapping .iter() - .filter(|mapping| mapping.is_some()) + .filter(|mapping| !mapping.is_empty()) .count() })?; From 40d117b973d9d3cc7dec8bd6a398e812a9550cb5 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 09:53:51 -0400 Subject: [PATCH 17/31] use FromIterator --- .../src/types/call/arguments.rs | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index c4251133c46c6..9944ad993180d 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -31,31 +31,25 @@ pub(crate) struct CallArguments<'a, 'db> { } impl<'a, 'db> CallArguments<'a, 'db> { - fn new(iter: impl IntoIterator, Option>)>) -> Self { - let (arguments, types) = iter.into_iter().unzip(); - Self { arguments, types } - } - /// Create `CallArguments` from AST arguments pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self { - Self::new( - arguments - .arguments_source_order() - .map(|arg_or_keyword| match arg_or_keyword { - ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, - _ => Argument::Positional, - }, - ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { - if let Some(arg) = arg { - Argument::Keyword(&arg.id) - } else { - Argument::Keywords - } + arguments + .arguments_source_order() + .map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(arg) => match arg { + ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, + _ => Argument::Positional, + }, + ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { + if let Some(arg) = arg { + Argument::Keyword(&arg.id) + } else { + Argument::Keywords } - }) - .map(|argument| (argument, None)), - ) + } + }) + .map(|argument| (argument, None)) + .collect() } /// Create a [`CallArguments`] with no arguments. @@ -182,6 +176,16 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } +impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments<'a, 'db> { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, Option>)>, + { + let (arguments, types) = iter.into_iter().unzip(); + Self { arguments, types } + } +} + impl<'a> From>> for CallArguments<'a, '_> { fn from(arguments: Vec>) -> Self { let types = vec![None; arguments.len()]; From 5fdaed8654132c8d6d580908533bb2b7ba74ebd8 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 09:54:10 -0400 Subject: [PATCH 18/31] remove unused From --- crates/ty_python_semantic/src/types/call/arguments.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 9944ad993180d..802cb123fc91a 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -186,13 +186,6 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments< } } -impl<'a> From>> for CallArguments<'a, '_> { - fn from(arguments: Vec>) -> Self { - let types = vec![None; arguments.len()]; - Self { arguments, types } - } -} - /// Expands a type into its possible subtypes, if applicable. /// /// Returns [`None`] if the type cannot be expanded. From d389168acea5a3ffc795e25cc04823e10adc0667 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 09:55:30 -0400 Subject: [PATCH 19/31] debug assert lengths --- crates/ty_python_semantic/src/types/infer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 436375f492c05..31d4c3a9da0e2 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4740,6 +4740,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { arguments: &mut CallArguments<'a, 'db>, argument_forms: &[Option], ) { + debug_assert!( + ast_arguments.len() == arguments.len() && arguments.len() == argument_forms.len() + ); let iter = (arguments.iter_mut()) .zip(argument_forms.iter().copied()) .zip(ast_arguments.arguments_source_order()); From 900240b5acf6b0f76b134d7d3f95a4c8e0b1fabd Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 09:57:03 -0400 Subject: [PATCH 20/31] add asserting constructor --- .../ty_python_semantic/src/types/call/arguments.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 802cb123fc91a..b6307cf996790 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -31,6 +31,11 @@ pub(crate) struct CallArguments<'a, 'db> { } impl<'a, 'db> CallArguments<'a, 'db> { + fn new(arguments: Vec>, types: Vec>>) -> Self { + debug_assert!(arguments.len() == types.len()); + Self { arguments, types } + } + /// Create `CallArguments` from AST arguments pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self { arguments @@ -156,10 +161,10 @@ impl<'a, 'db> CallArguments<'a, 'db> { for subtype in &expanded_types { let mut new_expanded_types = pre_expanded_types.to_vec(); new_expanded_types[index] = Some(*subtype); - expanded_arguments.push(CallArguments { - arguments: self.arguments.clone(), - types: new_expanded_types, - }); + expanded_arguments.push(CallArguments::new( + self.arguments.clone(), + new_expanded_types, + )); } } From 3a7c04de3167cf31480376ec186184ec40141122 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 10:00:34 -0400 Subject: [PATCH 21/31] add types iterator --- crates/ty_python_semantic/src/types/call/arguments.rs | 4 ++++ crates/ty_python_semantic/src/types/call/bind.rs | 3 +-- crates/ty_python_semantic/src/types/infer.rs | 7 +------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index b6307cf996790..89c3cf01127a7 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -77,6 +77,10 @@ impl<'a, 'db> CallArguments<'a, 'db> { &self.types } + pub(crate) fn iter_types(&self) -> impl Iterator> { + self.types.iter().map(|ty| ty.unwrap_or_else(Type::unknown)) + } + /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front /// of this argument list. (If `bound_self` is none, we return the argument list /// unmodified.) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index b79e98d73afa5..1919ae5a478c2 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1362,8 +1362,7 @@ impl<'db> CallableBinding<'db> { // participating parameter indexes. let mut top_materialized_argument_types = vec![]; - for (argument_index, (_, argument_type)) in arguments.iter().enumerate() { - let argument_type = argument_type.unwrap_or_else(Type::unknown); + for (argument_index, argument_type) in arguments.iter_types().enumerate() { let mut first_parameter_type: Option> = None; let mut participating_parameter_index = None; diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 31d4c3a9da0e2..118821548043f 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -7808,12 +7808,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); self.store_expression_type( slice_node, - TupleType::from_elements( - self.db(), - arguments - .iter() - .map(|(_, ty)| ty.unwrap_or_else(Type::unknown)), - ), + TupleType::from_elements(self.db(), arguments.iter_types()), ); arguments } From 8b65f345d9a5855a4d072cbd9d3d556445c21a9d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 11:07:47 -0400 Subject: [PATCH 22/31] use type alias for arg/param map --- crates/ty_python_semantic/src/types/call.rs | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 19 ++++++++++++------- .../src/types/ide_support.rs | 6 ++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index c34489bbbc9fa..0cd0d6fd2102b 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -5,7 +5,7 @@ use crate::Db; mod arguments; pub(crate) mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{Binding, Bindings, CallableBinding}; +pub(super) use bind::{ArgumentParameters, Binding, Bindings, CallableBinding}; /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was /// unsuccessful. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a1eed8fb354d9..21d5934a16bc3 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1735,7 +1735,7 @@ struct ArgumentMatcher<'a, 'db> { errors: &'a mut Vec>, /// The parameter that each argument is matched with. - argument_parameters: Vec>, + argument_parameters: Vec, /// Whether each parameter has been matched with an argument. parameter_matched: Vec, next_positional: usize, @@ -1882,7 +1882,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { Ok(()) } - fn finish(self) -> Box<[SmallVec<[usize; 1]>]> { + fn finish(self) -> Box<[ArgumentParameters]> { if let Some(first_excess_argument_index) = self.first_excess_positional { self.errors.push(BindingError::TooManyPositionalArguments { first_excess_argument_index: self.get_argument_index(first_excess_argument_index), @@ -1919,7 +1919,7 @@ struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, - argument_parameters: &'a [SmallVec<[usize; 1]>], + argument_parameters: &'a [ArgumentParameters], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, @@ -1932,7 +1932,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, - argument_parameters: &'a [SmallVec<[usize; 1]>], + argument_parameters: &'a [ArgumentParameters], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, ) -> Self { @@ -2132,6 +2132,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } +/// The index of the parameter(s) that an argument was matched against. This is tracked separately +/// for each overload. If an argument is not matched against any parameter, this indicates an +/// error. A variadic (splatted) argument might be matched against multiple parameters. +pub(crate) type ArgumentParameters = SmallVec<[usize; 1]>; + /// Binding information for one of the overloads of a callable. #[derive(Debug)] pub(crate) struct Binding<'db> { @@ -2157,7 +2162,7 @@ pub(crate) struct Binding<'db> { /// The formal parameter that each argument is matched with, in argument source order, or /// `None` if the argument was not matched to any parameter. - argument_parameters: Box<[SmallVec<[usize; 1]>]>, + argument_parameters: Box<[ArgumentParameters]>, /// Bound types for parameters, in parameter source order, or `None` if no argument was matched /// to that parameter. @@ -2346,7 +2351,7 @@ impl<'db> Binding<'db> { /// Returns a vector where each index corresponds to an argument position, /// and the value is the parameter index that argument maps to (if any). - pub(crate) fn argument_to_parameter_mapping(&self) -> &[SmallVec<[usize; 1]>] { + pub(crate) fn argument_to_parameter_mapping(&self) -> &[ArgumentParameters] { &self.argument_parameters } } @@ -2356,7 +2361,7 @@ struct BindingSnapshot<'db> { return_ty: Type<'db>, specialization: Option>, inherited_specialization: Option>, - argument_parameters: Box<[SmallVec<[usize; 1]>]>, + argument_parameters: Box<[ArgumentParameters]>, parameter_tys: Box<[Option>]>, errors: Vec>, } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 404e65354ec7b..c4bd433844602 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,7 +1,5 @@ use std::cmp::Ordering; -use smallvec::SmallVec; - use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations}; use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::DefinitionKind; @@ -9,7 +7,7 @@ use crate::semantic_index::place::ScopeId; use crate::semantic_index::{ attribute_scopes, global_scope, place_table, semantic_index, use_def_map, }; -use crate::types::call::CallArguments; +use crate::types::call::{ArgumentParameters, CallArguments}; use crate::types::signatures::Signature; use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type}; use crate::{Db, HasType, NameKind, SemanticModel}; @@ -411,7 +409,7 @@ pub struct CallSignatureDetails<'db> { /// Mapping from argument indices to parameter indices. This helps /// determine which parameter corresponds to which argument position. - pub argument_to_parameter_mapping: Vec>, + pub argument_to_parameter_mapping: Vec, } /// Extract signature details from a function call expression. From 043bcb64988433ac83bb81068a278915e5eeee42 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 15:07:42 -0400 Subject: [PATCH 23/31] better argument expansion regression test --- .../resources/mdtest/call/function.md | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index fd4fe4e6c60f3..e79b7b3a50894 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -226,22 +226,43 @@ to retry both `match_parameters` _and_ `check_types` for each expansion. Current The issue is that argument expansion might produce a splatted value with a different arity than what we originally inferred for the unexpanded value, and that in turn can affect which parameters the -splatted value is matched with. In this example, the ternary operator produces a complex union type, -which we expand when trying to call `range`. Our initial guess at its arity is "zero or more", but -there are individual union elements with more precise arities (such as "exactly two"). `range`, via -overloads and parameter defaults, can take in 1, 2, or 3 parameters. Our initial arity guess causes -us to assign the splatted argument to all three parameters. But when we check the `(0, 100)` union -element during argument expansion, we only have two values to provide for those three parameters. - -For now, we have a workaround that pads out the splatted value with `Unknown` when we encounter this -case, but a proper fix would retry parameter matching for each expanded union element. - -```py -def _(batch_ids=(0, 100)) -> None: - arg = batch_ids if isinstance(batch_ids, tuple) else (0, batch_ids) - # revealed: (Unknown & tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & tuple[Unknown, ...]) | tuple[Literal[0], (Unknown & ~tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & ~tuple[Unknown, ...])] - reveal_type(arg) - range(*arg) +splatted value is matched with. + +The first example correctly produces an error. The `tuple[int, str]` union element has a precise +arity of two, and so parameter matching chooses the first overload. The second element of the tuple +does not match the second parameter type, which yielding an `invalid-argument-type` error. + +The third example should produce the same error. However, because we have a union, we do not see the +precise arity of each union element during parameter matching. Instead, we infer an arity of "zero +or more" for the union as a whole, and use that less precise arity when matching parameters. We +therefore consider the second overload to still be a potential candidate for the `tuple[int, str]` +union element. During type checking, we have to force the arity of each union element to match the +inferred arity of the union as a whole (turning `tuple[int, str]` into `tuple[int | str, ...]`). +That less precise tuple type-checks successfully against the second overload, making us incorrectly +think that `tuple[int, str]` is a valid splatted call. + +If we update argument expansion to retry parameter matching with the precise arity of each union +element, we will correctly rule out the second overload for `tuple[int, str]`, just like we do when +splatting that tuple directly (instead of as part of a union). + +```py +from typing import overload + +@overload +def f(x: int, y: int) -> None: ... +@overload +def f(x: int, y: str, z: int) -> None: ... +def f(*args): ... + +def _(t: tuple[int, str]) -> None: + f(*t) # error: [invalid-argument-type] + +def _(t: tuple[int, str, int]) -> None: + f(*t) + +def _(t: tuple[int, str] | tuple[int, str, int]) -> None: + # TODO: error: [invalid-argument-type] + f(*t) ``` ## Wrong argument type From 57c9afcc79485fb15b133861a6ecea549b64d74d Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 15:21:29 -0400 Subject: [PATCH 24/31] add more arg type tests --- .../resources/mdtest/call/function.md | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index e79b7b3a50894..fc09f0ff4acff 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -77,13 +77,18 @@ def _(flag: bool): def takes_zero() -> None: ... def takes_one(x: int) -> None: ... def takes_two(x: int, y: int) -> None: ... +def takes_two_different(x: int, y: str) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... + +# Test all of the above with a number of different splatted argument types + def _(args: list[int]) -> None: takes_zero(*args) takes_one(*args) takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) @@ -92,6 +97,7 @@ def _(args: tuple[int, ...]) -> None: takes_zero(*args) takes_one(*args) takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) @@ -103,41 +109,39 @@ def _(args: tuple[int, ...]) -> None: def takes_zero() -> None: ... def takes_one(x: int) -> None: ... def takes_two(x: int, y: int) -> None: ... +def takes_two_different(x: int, y: str) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... + +# Test all of the above with a number of different splatted argument types + def _(args: tuple[int]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) - # error: [missing-argument] - takes_two(*args) + takes_two(*args) # error: [missing-argument] + takes_two_different(*args) # error: [missing-argument] takes_at_least_zero(*args) takes_at_least_one(*args) - # error: [missing-argument] - takes_at_least_two(*args) + takes_at_least_two(*args) # error: [missing-argument] def _(args: tuple[int, int]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) - # error: [too-many-positional-arguments] - takes_one(*args) + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) def _(args: tuple[int, str]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) - # error: [too-many-positional-arguments] - takes_one(*args) - # error: [invalid-argument-type] - takes_two(*args) + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) # error: [invalid-argument-type] + takes_two_different(*args) takes_at_least_zero(*args) takes_at_least_one(*args) - # error: [invalid-argument-type] - takes_at_least_two(*args) + takes_at_least_two(*args) # error: [invalid-argument-type] ``` ### Mixed tuple argument @@ -151,70 +155,66 @@ python-version = "3.11" def takes_zero() -> None: ... def takes_one(x: int) -> None: ... def takes_two(x: int, y: int) -> None: ... +def takes_two_different(x: int, y: str) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... + +# Test all of the above with a number of different splatted argument types + def _(args: tuple[int, *tuple[int, ...]]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) def _(args: tuple[int, *tuple[str, ...]]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) - # error: [invalid-argument-type] - takes_two(*args) + takes_two(*args) # error: [invalid-argument-type] + takes_two_different(*args) takes_at_least_zero(*args) takes_at_least_one(*args) - # error: [invalid-argument-type] - takes_at_least_two(*args) + takes_at_least_two(*args) # error: [invalid-argument-type] def _(args: tuple[int, int, *tuple[int, ...]]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) - # error: [too-many-positional-arguments] - takes_one(*args) + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) def _(args: tuple[int, int, *tuple[str, ...]]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) - # error: [too-many-positional-arguments] - takes_one(*args) + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) def _(args: tuple[int, *tuple[int, ...], int]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) - # error: [too-many-positional-arguments] - takes_one(*args) + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_different(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) def _(args: tuple[int, *tuple[str, ...], int]) -> None: - # error: [too-many-positional-arguments] - takes_zero(*args) - # error: [too-many-positional-arguments] - takes_one(*args) - # error: [invalid-argument-type] - takes_two(*args) + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) # error: [invalid-argument-type] + takes_two_different(*args) takes_at_least_zero(*args) takes_at_least_one(*args) - # error: [invalid-argument-type] - takes_at_least_two(*args) + takes_at_least_two(*args) # error: [invalid-argument-type] ``` ### Argument expansion regression From d16dbbbcf2d96b169b560f228e4deeffae265c98 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 16:06:43 -0400 Subject: [PATCH 25/31] MatchedArgument --- crates/ty_ide/src/signature_help.rs | 8 +- crates/ty_python_semantic/src/types/call.rs | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 90 ++++++++++--------- .../src/types/ide_support.rs | 6 +- 4 files changed, 58 insertions(+), 48 deletions(-) diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index 9d49c743bb056..b8232ebff2d5c 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details( details .argument_to_parameter_mapping .get(current_arg_index) - .and_then(|param_index| param_index.first().copied()) + .and_then(|mapping| mapping.parameters.first().copied()) .or({ // If we can't find a mapping for this argument, but we have a current // argument index, use that as the active parameter if it's within bounds. @@ -242,11 +242,11 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails] // First, try to find a signature where all arguments have valid parameter mappings. let perfect_match = signature_details.iter().position(|details| { - // Check if all arguments have valid parameter mappings (i.e., are not None). + // Check if all arguments have valid parameter mappings. details .argument_to_parameter_mapping .iter() - .all(|mapping| !mapping.is_empty()) + .all(|mapping| mapping.matched) }); if let Some(index) = perfect_match { @@ -261,7 +261,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails] details .argument_to_parameter_mapping .iter() - .filter(|mapping| !mapping.is_empty()) + .filter(|mapping| mapping.matched) .count() })?; diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index 0cd0d6fd2102b..be85b3922a72f 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -5,7 +5,7 @@ use crate::Db; mod arguments; pub(crate) mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{ArgumentParameters, Binding, Bindings, CallableBinding}; +pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument}; /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was /// unsuccessful. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 21d5934a16bc3..3e8ffbf76a134 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1369,7 +1369,7 @@ impl<'db> CallableBinding<'db> { for overload_index in matching_overload_indexes { let overload = &self.overloads[*overload_index]; - for parameter_index in &overload.argument_parameters[argument_index] { + for parameter_index in &overload.argument_matches[argument_index].parameters { // TODO: For an unannotated `self` / `cls` parameter, the type should be // `typing.Self` / `type[typing.Self]` let current_parameter_type = overload.signature.parameters()[*parameter_index] @@ -1410,7 +1410,7 @@ impl<'db> CallableBinding<'db> { let mut current_parameter_types = vec![]; for overload_index in &matching_overload_indexes[..=upto] { let overload = &self.overloads[*overload_index]; - for parameter_index in &overload.argument_parameters[argument_index] { + for parameter_index in &overload.argument_matches[argument_index].parameters { if !participating_parameter_indexes.contains(parameter_index) { // This parameter doesn't participate in the filtering process. continue; @@ -1734,9 +1734,7 @@ struct ArgumentMatcher<'a, 'db> { conflicting_forms: &'a mut [bool], errors: &'a mut Vec>, - /// The parameter that each argument is matched with. - argument_parameters: Vec, - /// Whether each parameter has been matched with an argument. + argument_matches: Vec, parameter_matched: Vec, next_positional: usize, first_excess_positional: Option, @@ -1756,7 +1754,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { argument_forms, conflicting_forms, errors, - argument_parameters: vec![smallvec![]; arguments.len()], + argument_matches: vec![MatchedArgument::default(); arguments.len()], parameter_matched: vec![false; parameters.len()], next_positional: 0, first_excess_positional: None, @@ -1801,7 +1799,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }); } } - self.argument_parameters[argument_index].push(parameter_index); + let matched_argument = &mut self.argument_matches[argument_index]; + matched_argument.parameters.push(parameter_index); + matched_argument.matched = true; self.parameter_matched[parameter_index] = true; } @@ -1882,7 +1882,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { Ok(()) } - fn finish(self) -> Box<[ArgumentParameters]> { + fn finish(self) -> Box<[MatchedArgument]> { if let Some(first_excess_argument_index) = self.first_excess_positional { self.errors.push(BindingError::TooManyPositionalArguments { first_excess_argument_index: self.get_argument_index(first_excess_argument_index), @@ -1911,7 +1911,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }); } - self.argument_parameters.into_boxed_slice() + self.argument_matches.into_boxed_slice() } } @@ -1919,7 +1919,7 @@ struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, - argument_parameters: &'a [ArgumentParameters], + argument_matches: &'a [MatchedArgument], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, @@ -1932,7 +1932,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, - argument_parameters: &'a [ArgumentParameters], + argument_matches: &'a [MatchedArgument], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, ) -> Self { @@ -1940,7 +1940,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { db, signature, arguments, - argument_parameters, + argument_matches, parameter_tys, errors, specialization: None, @@ -1987,7 +1987,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, _, argument_type) in self.enumerate_argument_types() { - for parameter_index in &self.argument_parameters[argument_index] { + for parameter_index in &self.argument_matches[argument_index].parameters { let parameter = ¶meters[*parameter_index]; let Some(expected_type) = parameter.annotated_type() else { continue; @@ -2058,7 +2058,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { { // If the argument isn't splatted, just check its type directly. let Argument::Variadic(_) = argument else { - for parameter_index in &self.argument_parameters[argument_index] { + for parameter_index in &self.argument_matches[argument_index].parameters { self.check_argument_type( adjusted_argument_index, argument, @@ -2105,17 +2105,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // failed, there might be more required elements. That means we can't use // TupleLength::Fixed below, because we would otherwise get a "too many values" error // when parameter matching failed. + let desired_size = + TupleLength::Variable(self.argument_matches[argument_index].parameters.len(), 0); let argument_types = argument_types - .resize( - self.db, - TupleLength::Variable(self.argument_parameters[argument_index].len(), 0), - ) + .resize(self.db, desired_size) .expect("argument type should be consistent with its arity"); // Check the types by zipping through the splatted argument types and their matched // parameters. - for (argument_type, parameter_index) in - (argument_types.all_elements()).zip(&self.argument_parameters[argument_index]) + for (argument_type, parameter_index) in (argument_types.all_elements()) + .zip(&self.argument_matches[argument_index].parameters) { self.check_argument_type( adjusted_argument_index, @@ -2132,10 +2131,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } -/// The index of the parameter(s) that an argument was matched against. This is tracked separately -/// for each overload. If an argument is not matched against any parameter, this indicates an -/// error. A variadic (splatted) argument might be matched against multiple parameters. -pub(crate) type ArgumentParameters = SmallVec<[usize; 1]>; +/// Information about which parameter(s) an argument was matched against. This is tracked +/// separately for each overload. +#[derive(Clone, Debug, Default)] +pub struct MatchedArgument { + /// The index of the parameter(s) that an argument was matched against. A splatted argument + /// might be matched against multiple parameters. + pub parameters: SmallVec<[usize; 1]>, + + /// Whether there were errors matching this argument. For a splatted argument, _all_ splatted + /// elements must have been successfully matched. (That means that this can be `false` while + /// the `parameters` field is non-empty.) + pub matched: bool, +} /// Binding information for one of the overloads of a callable. #[derive(Debug)] @@ -2160,9 +2168,9 @@ pub(crate) struct Binding<'db> { /// is being used to infer a specialization for the class. inherited_specialization: Option>, - /// The formal parameter that each argument is matched with, in argument source order, or - /// `None` if the argument was not matched to any parameter. - argument_parameters: Box<[ArgumentParameters]>, + /// Information about which parameter(s) each argument was matched with, in argument source + /// order. + argument_matches: Box<[MatchedArgument]>, /// Bound types for parameters, in parameter source order, or `None` if no argument was matched /// to that parameter. @@ -2181,7 +2189,7 @@ impl<'db> Binding<'db> { return_ty: Type::unknown(), specialization: None, inherited_specialization: None, - argument_parameters: Box::from([]), + argument_matches: Box::from([]), parameter_tys: Box::from([]), errors: vec![], } @@ -2226,7 +2234,7 @@ impl<'db> Binding<'db> { } self.return_ty = self.signature.return_ty.unwrap_or(Type::unknown()); self.parameter_tys = vec![None; parameters.len()].into_boxed_slice(); - self.argument_parameters = matcher.finish(); + self.argument_matches = matcher.finish(); } fn check_types(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { @@ -2234,7 +2242,7 @@ impl<'db> Binding<'db> { db, &self.signature, arguments, - &self.argument_parameters, + &self.argument_matches, &mut self.parameter_tys, &mut self.errors, ); @@ -2280,8 +2288,10 @@ impl<'db> Binding<'db> { ) -> impl Iterator, Type<'db>)> + 'a { argument_types .iter() - .zip(&self.argument_parameters) - .filter(move |(_, argument_parameters)| argument_parameters.contains(¶meter_index)) + .zip(&self.argument_matches) + .filter(move |(_, argument_matches)| { + argument_matches.parameters.contains(¶meter_index) + }) .map(|((argument, argument_type), _)| { (argument, argument_type.unwrap_or_else(Type::unknown)) }) @@ -2325,7 +2335,7 @@ impl<'db> Binding<'db> { return_ty: self.return_ty, specialization: self.specialization, inherited_specialization: self.inherited_specialization, - argument_parameters: self.argument_parameters.clone(), + argument_matches: self.argument_matches.clone(), parameter_tys: self.parameter_tys.clone(), errors: self.errors.clone(), } @@ -2336,7 +2346,7 @@ impl<'db> Binding<'db> { return_ty, specialization, inherited_specialization, - argument_parameters, + argument_matches, parameter_tys, errors, } = snapshot; @@ -2344,15 +2354,15 @@ impl<'db> Binding<'db> { self.return_ty = return_ty; self.specialization = specialization; self.inherited_specialization = inherited_specialization; - self.argument_parameters = argument_parameters; + self.argument_matches = argument_matches; self.parameter_tys = parameter_tys; self.errors = errors; } /// Returns a vector where each index corresponds to an argument position, /// and the value is the parameter index that argument maps to (if any). - pub(crate) fn argument_to_parameter_mapping(&self) -> &[ArgumentParameters] { - &self.argument_parameters + pub(crate) fn argument_matches(&self) -> &[MatchedArgument] { + &self.argument_matches } } @@ -2361,7 +2371,7 @@ struct BindingSnapshot<'db> { return_ty: Type<'db>, specialization: Option>, inherited_specialization: Option>, - argument_parameters: Box<[ArgumentParameters]>, + argument_matches: Box<[MatchedArgument]>, parameter_tys: Box<[Option>]>, errors: Vec>, } @@ -2402,8 +2412,8 @@ impl<'db> CallableBindingSnapshot<'db> { snapshot.specialization = binding.specialization; snapshot.inherited_specialization = binding.inherited_specialization; snapshot - .argument_parameters - .clone_from(&binding.argument_parameters); + .argument_matches + .clone_from(&binding.argument_matches); snapshot.parameter_tys.clone_from(&binding.parameter_tys); } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index c4bd433844602..beb69eb8d94fa 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -7,7 +7,7 @@ use crate::semantic_index::place::ScopeId; use crate::semantic_index::{ attribute_scopes, global_scope, place_table, semantic_index, use_def_map, }; -use crate::types::call::{ArgumentParameters, CallArguments}; +use crate::types::call::{CallArguments, MatchedArgument}; use crate::types::signatures::Signature; use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type}; use crate::{Db, HasType, NameKind, SemanticModel}; @@ -409,7 +409,7 @@ pub struct CallSignatureDetails<'db> { /// Mapping from argument indices to parameter indices. This helps /// determine which parameter corresponds to which argument position. - pub argument_to_parameter_mapping: Vec, + pub argument_to_parameter_mapping: Vec, } /// Extract signature details from a function call expression. @@ -448,7 +448,7 @@ pub fn call_signature_details<'db>( parameter_label_offsets, parameter_names, definition: signature.definition(), - argument_to_parameter_mapping: binding.argument_to_parameter_mapping().to_vec(), + argument_to_parameter_mapping: binding.argument_matches().to_vec(), } }) .collect() From 41447e3a9d20aafa36f711ce8206a7319a9560f7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 16:40:23 -0400 Subject: [PATCH 26/31] add tests from dhruv --- .../resources/mdtest/call/overloads.md | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index a66e28ad7e366..8144300bf8e6e 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -445,7 +445,34 @@ def _(ab: A | B, ac: A | C, cd: C | D): ## Filtering overloads with variadic arguments and parameters -TODO +### Non-participating gradual parameter + +`overloaded.pyi`: + +```pyi +from typing import Any, Literal, overload + +@overload +def f(x: tuple[str, Any], flag: Literal[True]) -> int: ... +@overload +def f(x: tuple[str, Any], flag: Literal[False] = ...) -> str: ... +@overload +def f(x: tuple[str, Any], flag: bool = ...) -> int | str: ... +``` + +```py +from typing import Any, Literal + +from overloaded import f + +def _(args: tuple[Any, Literal[True]]): + # TODO: revealed: int + reveal_type(f(*args)) # revealed: Unknown + +def _(args: tuple[Any, Literal[False]]): + # TODO: revealed: str + reveal_type(f(*args)) # revealed: Unknown +``` ## Filtering based on `Any` / `Unknown` @@ -740,7 +767,54 @@ def _(b_int: B[int], b_str: B[str], b_any: B[Any]): ### Variadic argument -TODO: A variadic parameter is being assigned to a number of parameters of the same type +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +class A: ... +class B: ... + +@overload +def f1(x: int) -> A: ... +@overload +def f1(x: Any, y: Any) -> A: ... + +@overload +def f2(x: int) -> A: ... +@overload +def f2(x: Any, y: Any) -> B: ... + +@overload +def f3(x: int) -> A: ... +@overload +def f3(x: Any, y: Any) -> A: ... +@overload +def f3(x: Any, y: Any, *, z: str) -> B: ... + +@overload +def f4(x: int) -> A: ... +@overload +def f4(x: Any, y: Any) -> B: ... +@overload +def f4(x: Any, y: Any, *, z: str) -> B: ... +``` + +```py +from typing import Any + +from overloaded import f1, f2, f3, f4 + +def _(arg: list[Any]): + # Matches both overload and the return types are equivalent + reveal_type(f1(*arg)) # revealed: A + # Matches both overload but the return types aren't equivalent + reveal_type(f2(*arg)) # revealed: Unknown + # Filters out the final overload and the return types are equivalent + reveal_type(f3(*arg)) # revealed: A + # Filters out the final overload but the return types aren't equivalent + reveal_type(f4(*arg)) # revealed: Unknown +``` ### Non-participating fully-static parameter From ec219b4f4c0f79837aa34ef7522204a0eb40f43a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 16:40:42 -0400 Subject: [PATCH 27/31] break out of the right loop --- crates/ty_python_semantic/src/types/call/bind.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 3e8ffbf76a134..e1fd55c341f8a 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1367,7 +1367,7 @@ impl<'db> CallableBinding<'db> { let mut first_parameter_type: Option> = None; let mut participating_parameter_index = None; - for overload_index in matching_overload_indexes { + 'overload: for overload_index in matching_overload_indexes { let overload = &self.overloads[*overload_index]; for parameter_index in &overload.argument_matches[argument_index].parameters { // TODO: For an unannotated `self` / `cls` parameter, the type should be @@ -1378,7 +1378,7 @@ impl<'db> CallableBinding<'db> { if let Some(first_parameter_type) = first_parameter_type { if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { participating_parameter_index = Some(*parameter_index); - break; + break 'overload; } } else { first_parameter_type = Some(current_parameter_type); From 3290874e9a7c6898a637f5b436083d8f503f86ae Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 16:47:32 -0400 Subject: [PATCH 28/31] add positional-only tests --- .../resources/mdtest/call/function.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index fc09f0ff4acff..722a57d18002b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -77,10 +77,13 @@ def _(flag: bool): def takes_zero() -> None: ... def takes_one(x: int) -> None: ... def takes_two(x: int, y: int) -> None: ... +def takes_two_positional_only(x: int, y: int, /) -> None: ... def takes_two_different(x: int, y: str) -> None: ... +def takes_two_different_positional_only(x: int, y: str, /) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... +def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... # Test all of the above with a number of different splatted argument types @@ -88,19 +91,25 @@ def _(args: list[int]) -> None: takes_zero(*args) takes_one(*args) takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) def _(args: tuple[int, ...]) -> None: takes_zero(*args) takes_one(*args) takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) ``` ### Fixed-length tuple argument @@ -109,10 +118,13 @@ def _(args: tuple[int, ...]) -> None: def takes_zero() -> None: ... def takes_one(x: int) -> None: ... def takes_two(x: int, y: int) -> None: ... +def takes_two_positional_only(x: int, y: int, /) -> None: ... def takes_two_different(x: int, y: str) -> None: ... +def takes_two_different_positional_only(x: int, y: str, /) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... +def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... # Test all of the above with a number of different splatted argument types @@ -120,28 +132,37 @@ def _(args: tuple[int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) takes_two(*args) # error: [missing-argument] + takes_two_positional_only(*args) # error: [missing-argument] takes_two_different(*args) # error: [missing-argument] + takes_two_different_positional_only(*args) # error: [missing-argument] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) # error: [missing-argument] + takes_at_least_two_positional_only(*args) # error: [missing-argument] def _(args: tuple[int, int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) def _(args: tuple[int, str]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] takes_two_different(*args) + takes_two_different_positional_only(*args) takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] ``` ### Mixed tuple argument @@ -155,10 +176,13 @@ python-version = "3.11" def takes_zero() -> None: ... def takes_one(x: int) -> None: ... def takes_two(x: int, y: int) -> None: ... +def takes_two_positional_only(x: int, y: int, /) -> None: ... def takes_two_different(x: int, y: str) -> None: ... +def takes_two_different_positional_only(x: int, y: str, /) -> None: ... def takes_at_least_zero(*args) -> None: ... def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... +def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... # Test all of the above with a number of different splatted argument types @@ -166,55 +190,73 @@ def _(args: tuple[int, *tuple[int, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) def _(args: tuple[int, *tuple[str, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] takes_two_different(*args) + takes_two_different_positional_only(*args) takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] def _(args: tuple[int, int, *tuple[int, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) def _(args: tuple[int, int, *tuple[str, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) def _(args: tuple[int, *tuple[int, ...], int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) + takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) def _(args: tuple[int, *tuple[str, ...], int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] takes_two_different(*args) + takes_two_different_positional_only(*args) takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] ``` ### Argument expansion regression From 369ef050f8ff5d05d4ea55b05fda90fad9757bcf Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Jul 2025 16:49:27 -0400 Subject: [PATCH 29/31] mdlint --- .../resources/mdtest/call/function.md | 96 ++++++++++--------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 722a57d18002b..30d636bedeca6 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -92,8 +92,8 @@ def _(args: list[int]) -> None: takes_one(*args) takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) @@ -104,8 +104,8 @@ def _(args: tuple[int, ...]) -> None: takes_one(*args) takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) @@ -129,40 +129,40 @@ def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... # Test all of the above with a number of different splatted argument types def _(args: tuple[int]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) - takes_two(*args) # error: [missing-argument] - takes_two_positional_only(*args) # error: [missing-argument] - takes_two_different(*args) # error: [missing-argument] - takes_two_different_positional_only(*args) # error: [missing-argument] + takes_two(*args) # error: [missing-argument] + takes_two_positional_only(*args) # error: [missing-argument] + takes_two_different(*args) # error: [missing-argument] + takes_two_different_positional_only(*args) # error: [missing-argument] takes_at_least_zero(*args) takes_at_least_one(*args) - takes_at_least_two(*args) # error: [missing-argument] - takes_at_least_two_positional_only(*args) # error: [missing-argument] + takes_at_least_two(*args) # error: [missing-argument] + takes_at_least_two_positional_only(*args) # error: [missing-argument] def _(args: tuple[int, int]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] - takes_one(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) takes_at_least_two_positional_only(*args) def _(args: tuple[int, str]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] - takes_one(*args) # error: [too-many-positional-arguments] - takes_two(*args) # error: [invalid-argument-type] - takes_two_positional_only(*args) # error: [invalid-argument-type] + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] takes_two_different(*args) takes_two_different_positional_only(*args) takes_at_least_zero(*args) takes_at_least_one(*args) - takes_at_least_two(*args) # error: [invalid-argument-type] - takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] ``` ### Mixed tuple argument @@ -187,76 +187,76 @@ def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... # Test all of the above with a number of different splatted argument types def _(args: tuple[int, *tuple[int, ...]]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) takes_at_least_two_positional_only(*args) def _(args: tuple[int, *tuple[str, ...]]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) - takes_two(*args) # error: [invalid-argument-type] - takes_two_positional_only(*args) # error: [invalid-argument-type] + takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] takes_two_different(*args) takes_two_different_positional_only(*args) takes_at_least_zero(*args) takes_at_least_one(*args) - takes_at_least_two(*args) # error: [invalid-argument-type] - takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] def _(args: tuple[int, int, *tuple[int, ...]]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] - takes_one(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) takes_at_least_two_positional_only(*args) def _(args: tuple[int, int, *tuple[str, ...]]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] - takes_one(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) takes_at_least_two_positional_only(*args) def _(args: tuple[int, *tuple[int, ...], int]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] - takes_one(*args) # error: [too-many-positional-arguments] + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] takes_at_least_zero(*args) takes_at_least_one(*args) takes_at_least_two(*args) takes_at_least_two_positional_only(*args) def _(args: tuple[int, *tuple[str, ...], int]) -> None: - takes_zero(*args) # error: [too-many-positional-arguments] - takes_one(*args) # error: [too-many-positional-arguments] - takes_two(*args) # error: [invalid-argument-type] - takes_two_positional_only(*args) # error: [invalid-argument-type] + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] takes_two_different(*args) takes_two_different_positional_only(*args) takes_at_least_zero(*args) takes_at_least_one(*args) - takes_at_least_two(*args) # error: [invalid-argument-type] - takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] ``` ### Argument expansion regression @@ -296,6 +296,8 @@ def f(x: int, y: int) -> None: ... def f(x: int, y: str, z: int) -> None: ... def f(*args): ... +# Test all of the above with a number of different splatted argument types + def _(t: tuple[int, str]) -> None: f(*t) # error: [invalid-argument-type] From abd0c9deb78467bccd9d987f6b205750b6f58e52 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 22 Jul 2025 14:11:44 -0400 Subject: [PATCH 30/31] add overload equivalent of every test --- .../resources/mdtest/call/overloads.md | 198 ++++++++++++++---- 1 file changed, 163 insertions(+), 35 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 6644934d63382..11b972f02465d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -5,6 +5,12 @@ with one or more overloads. This document describes the algorithm that it uses f matching, which is the same as the one mentioned in the [spec](https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation). +Note that all of the examples that involve positional parameters are tested multiple times: once +with the parameters matched with individual positional arguments, and once with the parameters +matched with a single positional argument that is splatted into the argument list. Overload +resolution is performed _after_ splatted arguments have been expanded, and so both approaches (TODO: +should) produce the same results. + ## Arity check The first step is to perform arity check. The non-overloaded cases are described in the @@ -26,10 +32,15 @@ from overloaded import f # These match a single overload reveal_type(f()) # revealed: None +reveal_type(f(*())) # revealed: None + reveal_type(f(1)) # revealed: int +reveal_type(f(*(1,))) # revealed: int # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f("a", "b")) # revealed: Unknown +# error: [no-matching-overload] "No overload of function `f` matches arguments" +reveal_type(f(*("a", "b"))) # revealed: Unknown ``` ## Type checking @@ -59,8 +70,13 @@ which filters out all but the matching overload: from overloaded import f reveal_type(f(1)) # revealed: int +reveal_type(f(*(1,))) # revealed: int + reveal_type(f("a")) # revealed: str +reveal_type(f(*("a",))) # revealed: str + reveal_type(f(b"b")) # revealed: bytes +reveal_type(f(*(b"b",))) # revealed: bytes ``` ### Single match error @@ -88,9 +104,12 @@ from typing_extensions import reveal_type from overloaded import f reveal_type(f()) # revealed: None +reveal_type(f(*())) # revealed: None # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`" reveal_type(f("a")) # revealed: Unknown +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`" +reveal_type(f(*("a",))) # revealed: Unknown ``` More examples of this diagnostic can be found in the @@ -117,10 +136,15 @@ from overloaded import A, B, f # These calls pass the arity check, and type checking matches both overloads: reveal_type(f(A())) # revealed: A +reveal_type(f(*(A(),))) # revealed: A + reveal_type(f(B())) # revealed: A +# TODO: revealed: A +reveal_type(f(*(B(),))) # revealed: Unknown # But, in this case, the arity check filters out the first overload, so we only have one match: reveal_type(f(B(), 1)) # revealed: B +reveal_type(f(*(B(), 1))) # revealed: B ``` ## Argument type expansion @@ -155,8 +179,13 @@ from overloaded import A, B, C, f def _(ab: A | B, ac: A | C, bc: B | C): reveal_type(f(ab)) # revealed: A | B + reveal_type(f(*(ab,))) # revealed: A | B + reveal_type(f(bc)) # revealed: B | C + reveal_type(f(*(bc,))) # revealed: B | C + reveal_type(f(ac)) # revealed: A | C + reveal_type(f(*(ac,))) # revealed: A | C ``` ### Expanding first argument @@ -189,11 +218,15 @@ from overloaded import A, B, C, D, f def _(a_b: A | B): reveal_type(f(a_b, C())) # revealed: A | C + reveal_type(f(*(a_b, C()))) # revealed: A | C + reveal_type(f(a_b, D())) # revealed: B | D + reveal_type(f(*(a_b, D()))) # revealed: B | D # But, if it doesn't, it should expand the second argument and try again: def _(a_b: A | B, c_d: C | D): reveal_type(f(a_b, c_d)) # revealed: A | B | C | D + reveal_type(f(*(a_b, c_d))) # revealed: A | B | C | D ``` ### Expanding second argument @@ -226,9 +259,12 @@ def _(a: A, bc: B | C, cd: C | D): # This also tests that partial matching works correctly as the argument type expansion results # in matching the first and second overloads, but not the third one. reveal_type(f(a, bc)) # revealed: B | C + reveal_type(f(*(a, bc))) # revealed: B | C # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f(a, cd)) # revealed: Unknown + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(*(a, cd))) # revealed: Unknown ``` ### Generics (legacy) @@ -254,7 +290,16 @@ from overloaded import A, f def _(x: int, y: A | int): reveal_type(f(x)) # revealed: int + # TODO: revealed: int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(x,))) # revealed: Unknown + reveal_type(f(y)) # revealed: A | int + # TODO: revealed: A | int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(y,))) # revealed: Unknown ``` ### Generics (PEP 695) @@ -283,7 +328,16 @@ from overloaded import B, f def _(x: int, y: B | int): reveal_type(f(x)) # revealed: int + # TODO: revealed: int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(x,))) # revealed: Unknown + reveal_type(f(y)) # revealed: B | int + # TODO: revealed: B | int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(y,))) # revealed: Unknown ``` ### Expanding `bool` @@ -307,8 +361,13 @@ from overloaded import f def _(flag: bool): reveal_type(f(True)) # revealed: T + reveal_type(f(*(True,))) # revealed: T + reveal_type(f(False)) # revealed: F + reveal_type(f(*(False,))) # revealed: F + reveal_type(f(flag)) # revealed: T | F + reveal_type(f(*(flag,))) # revealed: T | F ``` ### Expanding `tuple` @@ -338,6 +397,7 @@ from overloaded import A, B, f def _(x: tuple[A | B, int], y: tuple[int, bool]): reveal_type(f(x, y)) # revealed: A | B | C | D + reveal_type(f(*(x, y))) # revealed: A | B | C | D ``` ### Expanding `type` @@ -365,6 +425,7 @@ from overloaded import A, B, f def _(x: type[A | B]): reveal_type(x) # revealed: type[A] | type[B] reveal_type(f(x)) # revealed: A | B + reveal_type(f(*(x,))) # revealed: A | B ``` ### Expanding enums @@ -401,10 +462,19 @@ from overloaded import SomeEnum, A, B, C, f def _(x: SomeEnum, y: Literal[SomeEnum.A, SomeEnum.C]): reveal_type(f(SomeEnum.A)) # revealed: A + reveal_type(f(*(SomeEnum.A,))) # revealed: A + reveal_type(f(SomeEnum.B)) # revealed: B + reveal_type(f(*(SomeEnum.B,))) # revealed: B + reveal_type(f(SomeEnum.C)) # revealed: C + reveal_type(f(*(SomeEnum.C,))) # revealed: C + reveal_type(f(x)) # revealed: A | B | C + reveal_type(f(*(x,))) # revealed: A | B | C + reveal_type(f(y)) # revealed: A | C + reveal_type(f(*(y,))) # revealed: A | C ``` #### Enum with single member @@ -493,9 +563,19 @@ from overloaded import MyEnumSubclass, ActualEnum, f def _(actual_enum: ActualEnum, my_enum_instance: MyEnumSubclass): reveal_type(f(actual_enum)) # revealed: Both + # TODO: revealed: Both + reveal_type(f(*(actual_enum,))) # revealed: Unknown + reveal_type(f(ActualEnum.A)) # revealed: OnlyA + # TODO: revealed: OnlyA + reveal_type(f(*(ActualEnum.A,))) # revealed: Unknown + reveal_type(f(ActualEnum.B)) # revealed: OnlyB + # TODO: revealed: OnlyB + reveal_type(f(*(ActualEnum.B,))) # revealed: Unknown + reveal_type(f(my_enum_instance)) # revealed: MyEnumSubclass + reveal_type(f(*(my_enum_instance,))) # revealed: MyEnumSubclass ``` ### No matching overloads @@ -524,46 +604,20 @@ from overloaded import A, B, C, D, f def _(ab: A | B, ac: A | C, cd: C | D): reveal_type(f(ab)) # revealed: A | B + reveal_type(f(*(ab,))) # revealed: A | B # The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first # overload while the second list doesn't match any of the overloads, so we generate an # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f(ac)) # revealed: Unknown + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(*(ac,))) # revealed: Unknown # None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f(cd)) # revealed: Unknown -``` - -## Filtering overloads with variadic arguments and parameters - -### Non-participating gradual parameter - -`overloaded.pyi`: - -```pyi -from typing import Any, Literal, overload - -@overload -def f(x: tuple[str, Any], flag: Literal[True]) -> int: ... -@overload -def f(x: tuple[str, Any], flag: Literal[False] = ...) -> str: ... -@overload -def f(x: tuple[str, Any], flag: bool = ...) -> int | str: ... -``` - -```py -from typing import Any, Literal - -from overloaded import f - -def _(args: tuple[Any, Literal[True]]): - # TODO: revealed: int - reveal_type(f(*args)) # revealed: Unknown - -def _(args: tuple[Any, Literal[False]]): - # TODO: revealed: str - reveal_type(f(*args)) # revealed: Unknown + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(*(cd,))) # revealed: Unknown ``` ## Filtering based on `Any` / `Unknown` @@ -600,10 +654,16 @@ from overloaded import f # Anything other than `list` should match the last overload reveal_type(f(1)) # revealed: str +reveal_type(f(*(1,))) # revealed: str def _(list_int: list[int], list_any: list[Any]): reveal_type(f(list_int)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(list_int,))) # revealed: Unknown + reveal_type(f(list_any)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(list_any,))) # revealed: Unknown ``` ### Single list argument (ambiguous) @@ -631,16 +691,20 @@ from overloaded import f # Anything other than `list` should match the last overload reveal_type(f(1)) # revealed: str +reveal_type(f(*(1,))) # revealed: str def _(list_int: list[int], list_any: list[Any]): # All materializations of `list[int]` are assignable to `list[int]`, so it matches the first # overload. reveal_type(f(list_int)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(list_int,))) # revealed: Unknown # All materializations of `list[Any]` are assignable to `list[int]` and `list[Any]`, but the # return type of first and second overloads are not equivalent, so the overload matching # is ambiguous. reveal_type(f(list_any)) # revealed: Unknown + reveal_type(f(*(list_any,))) # revealed: Unknown ``` ### Single tuple argument @@ -664,21 +728,33 @@ from typing import Any from overloaded import f reveal_type(f("a")) # revealed: str +reveal_type(f(*("a",))) # revealed: str + reveal_type(f((1, "b"))) # revealed: int +# TODO: revealed: int +reveal_type(f(*((1, "b"),))) # revealed: Unknown + reveal_type(f((1, 2))) # revealed: int +# TODO: revealed: int +reveal_type(f(*((1, 2),))) # revealed: Unknown def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]): # All materializations are assignable to first overload, so second and third overloads are # eliminated reveal_type(f(int_str)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(int_str,))) # revealed: Unknown # All materializations are assignable to second overload, so the third overload is eliminated; # the return type of first and second overload is equivalent reveal_type(f(int_any)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(int_any,))) # revealed: Unknown # All materializations of `tuple[Any, Any]` are assignable to the parameters of all the # overloads, but the return types aren't equivalent, so the overload matching is ambiguous reveal_type(f(any_any)) # revealed: Unknown + reveal_type(f(*(any_any,))) # revealed: Unknown ``` ### Multiple arguments @@ -708,23 +784,32 @@ def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_an # All materializations of both argument types are assignable to the first overload, so the # second and third overloads are filtered out reveal_type(f(list_int, int_str)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_int, int_str))) # revealed: Unknown # All materialization of first argument is assignable to first overload and for the second # argument, they're assignable to the second overload, so the third overload is filtered out reveal_type(f(list_int, int_any)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_int, int_any))) # revealed: Unknown # All materialization of first argument is assignable to second overload and for the second # argument, they're assignable to the first overload, so the third overload is filtered out reveal_type(f(list_any, int_str)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_any, int_str))) # revealed: Unknown # All materializations of both arguments are assignable to the second overload, so the third # overload is filtered out reveal_type(f(list_any, int_any)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_any, int_any))) # revealed: Unknown # All materializations of first argument is assignable to the second overload and for the second # argument, they're assignable to the third overload, so no overloads are filtered out; the # return types of the remaining overloads are not equivalent, so overload matching is ambiguous reveal_type(f(list_int, any_any)) # revealed: Unknown + reveal_type(f(*(list_int, any_any))) # revealed: Unknown ``` ### `LiteralString` and `str` @@ -749,11 +834,16 @@ from overloaded import f def _(literal: LiteralString, string: str, any: Any): reveal_type(f(literal)) # revealed: LiteralString + # TODO: revealed: LiteralString + reveal_type(f(*(literal,))) # revealed: Unknown + reveal_type(f(string)) # revealed: str + reveal_type(f(*(string,))) # revealed: str # `Any` matches both overloads, but the return types are not equivalent. # Pyright and mypy both reveal `str` here, contrary to the spec. reveal_type(f(any)) # revealed: Unknown + reveal_type(f(*(any,))) # revealed: Unknown ``` ### Generics @@ -783,10 +873,19 @@ from overloaded import f def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any): reveal_type(f(list_int)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_int,))) # revealed: Unknown + # TODO: Should be `str` reveal_type(f(list_str)) # revealed: Unknown + # TODO: Should be `str` + reveal_type(f(*(list_str,))) # revealed: Unknown + reveal_type(f(list_any)) # revealed: Unknown + reveal_type(f(*(list_any,))) # revealed: Unknown + reveal_type(f(any)) # revealed: Unknown + reveal_type(f(*(any,))) # revealed: Unknown ``` ### Generics (multiple arguments) @@ -811,12 +910,24 @@ from overloaded import f def _(integer: int, string: str, any: Any, list_any: list[Any]): reveal_type(f(integer, string)) # revealed: int + reveal_type(f(*(integer, string))) # revealed: int + reveal_type(f(string, integer)) # revealed: int + # TODO: revealed: int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(string, integer))) # revealed: Unknown # This matches the second overload and is _not_ the case of ambiguous overload matching. reveal_type(f(string, any)) # revealed: Any + # TODO: Any + reveal_type(f(*(string, any))) # revealed: tuple[str, Any] reveal_type(f(string, list_any)) # revealed: list[Any] + # TODO: revealed: list[Any] + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(string, list_any))) # revealed: Unknown ``` ### Generic `self` @@ -942,7 +1053,10 @@ from overloaded import f def _(any: Any): reveal_type(f(any, flag=True)) # revealed: int + reveal_type(f(*(any,), flag=True)) # revealed: int + reveal_type(f(any, flag=False)) # revealed: str + reveal_type(f(*(any,), flag=False)) # revealed: str ``` ### Non-participating gradual parameter @@ -953,21 +1067,32 @@ def _(any: Any): from typing import Any, Literal, overload @overload -def f(x: tuple[str, Any], *, flag: Literal[True]) -> int: ... +def f(x: tuple[str, Any], flag: Literal[True]) -> int: ... @overload -def f(x: tuple[str, Any], *, flag: Literal[False] = ...) -> str: ... +def f(x: tuple[str, Any], flag: Literal[False] = ...) -> str: ... @overload -def f(x: tuple[str, Any], *, flag: bool = ...) -> int | str: ... +def f(x: tuple[str, Any], flag: bool = ...) -> int | str: ... ``` ```py -from typing import Any +from typing import Any, Literal from overloaded import f def _(any: Any): reveal_type(f(any, flag=True)) # revealed: int + reveal_type(f(*(any,), flag=True)) # revealed: int + reveal_type(f(any, flag=False)) # revealed: str + reveal_type(f(*(any,), flag=False)) # revealed: str + +def _(args: tuple[Any, Literal[True]]): + # TODO: revealed: int + reveal_type(f(*args)) # revealed: Unknown + +def _(args: tuple[Any, Literal[False]]): + # TODO: revealed: str + reveal_type(f(*args)) # revealed: Unknown ``` ### Argument type expansion @@ -1012,6 +1137,7 @@ from overloaded import A, B, f def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: A | B + reveal_type(f(*(arg,))) # revealed: A | B ``` #### One argument list ambiguous @@ -1046,6 +1172,7 @@ from overloaded import A, B, C, f def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: A | Unknown + reveal_type(f(*(arg,))) # revealed: A | Unknown ``` #### Both argument lists ambiguous @@ -1079,4 +1206,5 @@ from overloaded import A, B, C, f def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: Unknown + reveal_type(f(*(arg,))) # revealed: Unknown ``` From b1b6087740ae3fab3a6cbdcde69d32479cfe146f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 22 Jul 2025 14:15:20 -0400 Subject: [PATCH 31/31] mdlint --- crates/ty_python_semantic/resources/mdtest/call/overloads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 11b972f02465d..8d1f2930cdf44 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -529,7 +529,7 @@ def _(missing: Literal[Missing.Value], missing_or_present: Literal[Missing.Value #### Enum subclass without members -An `Enum` subclass without members should *not* be expanded: +An `Enum` subclass without members should _not_ be expanded: `overloaded.pyi`: