From 76ffc56e850a80f45e0d8c59c8df712a4fd0587a Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 12 Dec 2025 16:23:48 +0530 Subject: [PATCH] [ty] Add basic support for overloads in `ParamSpec` --- .../mdtest/generics/pep695/paramspec.md | 12 +++- .../ty_python_semantic/src/types/call/bind.rs | 65 +++++++++++++++---- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 92e1e64116..33fa647ed7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -664,9 +664,15 @@ reveal_type(change_return_type(int_str)) # revealed: Overload[(x: int) -> str, # error: [invalid-argument-type] reveal_type(change_return_type(str_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str] -# TODO: Both of these shouldn't raise an error -# error: [invalid-argument-type] +# TODO: This should reveal the matching overload instead reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str, (x: str) -> str] -# error: [invalid-argument-type] reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str] + +# error: [invalid-argument-type] "Argument to function `with_parameters` is incorrect: Expected `int`, found `None`" +reveal_type(with_parameters(int_int, None)) # revealed: Overload[(x: int) -> str, (x: str) -> str] + +def foo(int_or_str: int | str): + # Argument type expansion leads to matching both overloads. + # TODO: Should this be an error instead? + reveal_type(with_parameters(int_int, int_or_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str] ``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e81d26d8b8..e71fbe8ca0 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3313,8 +3313,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// are passed. /// /// This method returns `false` if the specialization does not contain a mapping for the given - /// `paramspec`, contains an invalid mapping (i.e., not a `Callable` of kind `ParamSpecValue`) - /// or if the value is an overloaded callable. + /// `paramspec` or contains an invalid mapping (i.e., not a `Callable` of kind `ParamSpecValue`). /// /// For more details, refer to [`Self::try_paramspec_evaluation_at`]. fn evaluate_paramspec_sub_call( @@ -3333,10 +3332,10 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; } - // TODO: Support overloads? - let [signature] = callable.signatures(self.db).overloads.as_slice() else { + let signatures = &callable.signatures(self.db).overloads; + if signatures.is_empty() { return false; - }; + } let sub_arguments = if let Some(argument_index) = argument_index { self.arguments.start_from(argument_index) @@ -3344,21 +3343,61 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { CallArguments::none() }; - // TODO: What should be the `signature_type` here? - let bindings = match Bindings::from(Binding::single(self.signature_type, signature.clone())) + // Create Bindings with all overloads and perform full overload resolution + let callable_binding = + CallableBinding::from_overloads(self.signature_type, signatures.iter().cloned()); + let bindings = match Bindings::from(callable_binding) .match_parameters(self.db, &sub_arguments) .check_types(self.db, &sub_arguments, self.call_expression_tcx, &[]) { - Ok(bindings) => Box::new(bindings), - Err(CallError(_, bindings)) => bindings, + Ok(bindings) => bindings, + Err(CallError(_, bindings)) => *bindings, }; - // SAFETY: `bindings` was created from a single binding above. - let [binding] = bindings.single_element().unwrap().overloads.as_slice() else { - unreachable!("ParamSpec sub-call should only contain a single binding"); + // SAFETY: `bindings` was created from a single `CallableBinding` above. + let Some(callable_binding) = bindings.single_element() else { + unreachable!("ParamSpec sub-call should only contain a single CallableBinding"); }; - self.errors.extend(binding.errors.iter().cloned()); + match callable_binding.matching_overload_index() { + MatchingOverloadIndex::None => { + if let [binding] = callable_binding.overloads() { + // This is not an overloaded function, so we can propagate its errors + // to the outer bindings. + self.errors.extend(binding.errors.iter().cloned()); + } else { + let index = callable_binding + .matching_overload_before_type_checking + .unwrap_or(0); + // TODO: We should also update the specialization for the `ParamSpec` to reflect + // the matching overload here. + self.errors + .extend(callable_binding.overloads()[index].errors.iter().cloned()); + } + } + MatchingOverloadIndex::Single(index) => { + // TODO: We should also update the specialization for the `ParamSpec` to reflect the + // matching overload here. + self.errors + .extend(callable_binding.overloads()[index].errors.iter().cloned()); + } + MatchingOverloadIndex::Multiple(_) => { + if !matches!( + callable_binding.overload_call_return_type, + Some(OverloadCallReturnType::ArgumentTypeExpansion(_)) + ) { + self.errors.extend( + callable_binding + .overloads() + .first() + .unwrap() + .errors + .iter() + .cloned(), + ); + } + } + } true }