[ty] Splat variadic arguments into parameter list (#18996)

This PR updates our call binding logic to handle splatted arguments.

Complicating matters is that we have separated call bind analysis into
two phases: parameter matching and type checking. Parameter matching
looks at the arity of the function signature and call site, and assigns
arguments to parameters. Importantly, we don't yet know the type of each
argument! This is needed so that we can decide whether to infer the type
of each argument as a type form or value form, depending on the
requirements of the parameter that the argument was matched to.

This is an issue when splatting an argument, since we need to know how
many elements the splatted argument contains to know how many positional
parameters to match it against. And to know how many elements the
splatted argument has, we need to know its type.

To get around this, we now make the assumption that splatted arguments
can only be used with value-form parameters. (If you end up splatting an
argument into a type-form parameter, we will silently pass in its
value-form type instead.) That allows us to preemptively infer the
(value-form) type of any splatted argument, so that we have its arity
available during parameter matching. We defer inference of non-splatted
arguments until after parameter matching has finished, as before.

We reuse a lot of the new tuple machinery to make this happen — in
particular resizing the tuple spec representing the number of arguments
passed in with the tuple length representing the number of parameters
the splat was matched with.

This work also shows that we might need to change how we are performing
argument expansion during overload resolution. At the moment, when we
expand parameters, we assume that each argument will still be matched to
the same parameters as before, and only retry the type-checking phase.
With splatted arguments, this is no longer the case, since the inferred
arity of each union element might be different than the arity of the
union as a whole, which can affect how many parameters the splatted
argument is matched to. See the regression test case in
`mdtest/call/function.md` for more details.
This commit is contained in:
Douglas Creager 2025-07-22 14:33:08 -04:00 committed by GitHub
parent 9d5ecacdc5
commit 7673d46b71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 694 additions and 130 deletions

View file

@ -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::{Binding, Bindings, CallableBinding, MatchedArgument};
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
/// unsuccessful.

View file

@ -6,7 +6,7 @@ use ruff_python_ast as ast;
use crate::Db;
use crate::types::KnownClass;
use crate::types::enums::enum_member_literals;
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::tuple::{TupleLength, TupleSpec, TupleType};
use super::Type;
@ -16,8 +16,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`).
@ -37,24 +37,38 @@ impl<'a, 'db> CallArguments<'a, 'db> {
Self { arguments, types }
}
/// Create `CallArguments` from AST arguments
pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self {
/// Create `CallArguments` from AST arguments. We will use the provided callback to obtain the
/// type of each splatted argument, so that we can determine its length. All other arguments
/// will remain uninitialized as `Unknown`.
pub(crate) fn from_arguments(
db: &'db dyn Db,
arguments: &'a ast::Arguments,
mut infer_argument_type: impl FnMut(&ast::Expr, &ast::Expr) -> Type<'db>,
) -> 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::Expr::Starred(ast::ExprStarred { value, .. }) => {
let ty = infer_argument_type(arg, value);
let length = match ty {
Type::Tuple(tuple) => tuple.tuple(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))
}
_ => (Argument::Positional, None),
},
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
if let Some(arg) = arg {
Argument::Keyword(&arg.id)
(Argument::Keyword(&arg.id), None)
} else {
Argument::Keywords
(Argument::Keywords, None)
}
}
})
.map(|argument| (argument, None))
.collect()
}

View file

@ -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;
@ -25,7 +26,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,
@ -1388,24 +1389,22 @@ impl<'db> CallableBinding<'db> {
let mut first_parameter_type: Option<Type<'db>> = 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];
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_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]
.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 'overload;
}
} else {
first_parameter_type = Some(current_parameter_type);
}
} else {
first_parameter_type = Some(current_parameter_type);
}
}
@ -1433,20 +1432,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(&parameter_index) {
// This parameter doesn't participate in the filtering process.
continue;
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;
}
// 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,9 +1758,7 @@ struct ArgumentMatcher<'a, 'db> {
conflicting_forms: &'a mut [bool],
errors: &'a mut Vec<BindingError<'db>>,
/// The parameter that each argument is matched with.
argument_parameters: Vec<Option<usize>>,
/// Whether each parameter has been matched with an argument.
argument_matches: Vec<MatchedArgument>,
parameter_matched: Vec<bool>,
next_positional: usize,
first_excess_positional: Option<usize>,
@ -1783,7 +1778,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
argument_forms,
conflicting_forms,
errors,
argument_parameters: vec![None; arguments.len()],
argument_matches: vec![MatchedArgument::default(); arguments.len()],
parameter_matched: vec![false; parameters.len()],
next_positional: 0,
first_excess_positional: None,
@ -1828,7 +1823,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
});
}
}
self.argument_parameters[argument_index] = Some(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 +1879,34 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
Ok(())
}
fn finish(self) -> Box<[Option<usize>]> {
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<[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 +1935,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
});
}
self.argument_parameters.into_boxed_slice()
self.argument_matches.into_boxed_slice()
}
}
@ -1919,7 +1943,7 @@ struct ArgumentTypeChecker<'a, 'db> {
db: &'db dyn Db,
signature: &'a Signature<'db>,
arguments: &'a CallArguments<'a, 'db>,
argument_parameters: &'a [Option<usize>],
argument_matches: &'a [MatchedArgument],
parameter_tys: &'a mut [Option<Type<'db>>],
errors: &'a mut Vec<BindingError<'db>>,
@ -1932,7 +1956,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
db: &'db dyn Db,
signature: &'a Signature<'db>,
arguments: &'a CallArguments<'a, 'db>,
argument_parameters: &'a [Option<usize>],
argument_matches: &'a [MatchedArgument],
parameter_tys: &'a mut [Option<Type<'db>>],
errors: &'a mut Vec<BindingError<'db>>,
) -> Self {
@ -1940,7 +1964,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
db,
signature,
arguments,
argument_parameters,
argument_matches,
parameter_tys,
errors,
specialization: None,
@ -1987,20 +2011,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 = &parameters[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_matches[argument_index].parameters {
let parameter = &parameters[*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));
@ -2016,16 +2037,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
fn check_argument_type(
&mut self,
argument_index: usize,
adjusted_argument_index: Option<usize>,
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 = &parameters[parameter_index];
if let Some(mut expected_ty) = parameter.annotated_type() {
@ -2064,12 +2080,73 @@ 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,
);
// If the argument isn't splatted, just check its type directly.
let Argument::Variadic(_) = argument else {
for parameter_index in &self.argument_matches[argument_index].parameters {
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))
}
};
// 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 (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 desired_size =
TupleLength::Variable(self.argument_matches[argument_index].parameters.len(), 0);
let argument_types = argument_types
.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_matches[argument_index].parameters)
{
self.check_argument_type(
adjusted_argument_index,
argument,
*argument_type,
*parameter_index,
);
}
}
}
@ -2078,6 +2155,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
}
}
/// 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)]
pub(crate) struct Binding<'db> {
@ -2101,9 +2192,9 @@ pub(crate) struct Binding<'db> {
/// is being used to infer a specialization for the class.
inherited_specialization: Option<Specialization<'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<usize>]>,
/// 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.
@ -2122,7 +2213,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![],
}
@ -2156,7 +2247,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;
}
@ -2164,7 +2258,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>) {
@ -2172,7 +2266,7 @@ impl<'db> Binding<'db> {
db,
&self.signature,
arguments,
&self.argument_parameters,
&self.argument_matches,
&mut self.parameter_tys,
&mut self.errors,
);
@ -2218,9 +2312,9 @@ impl<'db> Binding<'db> {
) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + 'a {
argument_types
.iter()
.zip(&self.argument_parameters)
.filter(move |(_, argument_parameter)| {
argument_parameter.is_some_and(|ap| ap == parameter_index)
.zip(&self.argument_matches)
.filter(move |(_, argument_matches)| {
argument_matches.parameters.contains(&parameter_index)
})
.map(|((argument, argument_type), _)| {
(argument, argument_type.unwrap_or_else(Type::unknown))
@ -2265,7 +2359,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(),
}
@ -2276,7 +2370,7 @@ impl<'db> Binding<'db> {
return_ty,
specialization,
inherited_specialization,
argument_parameters,
argument_matches,
parameter_tys,
errors,
} = snapshot;
@ -2284,15 +2378,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) -> &[Option<usize>] {
&self.argument_parameters
pub(crate) fn argument_matches(&self) -> &[MatchedArgument] {
&self.argument_matches
}
}
@ -2301,7 +2395,7 @@ struct BindingSnapshot<'db> {
return_ty: Type<'db>,
specialization: Option<Specialization<'db>>,
inherited_specialization: Option<Specialization<'db>>,
argument_parameters: Box<[Option<usize>]>,
argument_matches: Box<[MatchedArgument]>,
parameter_tys: Box<[Option<Type<'db>>]>,
errors: Vec<BindingError<'db>>,
}
@ -2342,8 +2436,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);
}

View file

@ -9,7 +9,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::{CallArguments, MatchedArgument};
use crate::types::signatures::Signature;
use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type};
use crate::{Db, HasType, NameKind, SemanticModel};
@ -724,8 +724,8 @@ pub struct CallSignatureDetails<'db> {
pub definition: Option<Definition<'db>>,
/// Mapping from argument indices to parameter indices. This helps
/// identify which argument corresponds to which parameter.
pub argument_to_parameter_mapping: Vec<Option<usize>>,
/// determine which parameter corresponds to which argument position.
pub argument_to_parameter_mapping: Vec<MatchedArgument>,
}
/// Extract signature details from a function call expression.
@ -742,7 +742,10 @@ pub fn call_signature_details<'db>(
// Use into_callable to handle all the complex type conversions
if let Some(callable_type) = func_type.into_callable(db) {
let call_arguments = CallArguments::from_arguments(&call_expr.arguments);
let call_arguments =
CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| {
splatted_value.inferred_type(&model)
});
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
// Extract signature details from all callable bindings
@ -761,7 +764,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()

View file

@ -2203,7 +2203,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_parameters(type_params);
if let Some(arguments) = class.arguments.as_deref() {
let mut call_arguments = CallArguments::from_arguments(arguments);
let mut call_arguments =
CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| {
let ty = self.infer_expression(splatted_value);
self.store_expression_type(argument, ty);
ty
});
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
}
@ -5165,19 +5170,13 @@ 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 {
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);
self.store_expression_type(arg, ty);
ty
}
_ => self.infer_argument_type(arg, form),
},
ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => {
self.infer_argument_type(value, form)
}
let argument = match arg_or_keyword {
// We already inferred the type of splatted arguments.
ast::ArgOrKeyword::Arg(ast::Expr::Starred(_)) => continue,
ast::ArgOrKeyword::Arg(arg) => arg,
ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => value,
};
let ty = self.infer_argument_type(argument, form);
*argument_type = Some(ty);
}
}
@ -5874,7 +5873,12 @@ 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 mut call_arguments = CallArguments::from_arguments(arguments);
let mut call_arguments =
CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| {
let ty = self.infer_expression(splatted_value);
self.store_expression_type(argument, ty);
ty
});
let callable_type = self.infer_maybe_standalone_expression(func);

View file

@ -38,6 +38,14 @@ pub(crate) enum TupleLength {
}
impl TupleLength {
pub(crate) const fn unknown() -> 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<usize>) {