From 8d7a21166e33d83d0b43b3d31f235aae785dd389 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 7 Mar 2025 14:01:15 -0500 Subject: [PATCH 01/32] Add CallBinding::into_outcome --- crates/red_knot_python_semantic/src/types.rs | 100 +++++++----------- .../src/types/call/bind.rs | 9 +- 2 files changed, 46 insertions(+), 63 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 8fe87b8bae3f2..f97884d3fe794 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1883,19 +1883,13 @@ impl<'db> Type<'db> { Type::Callable(CallableType::BoundMethod(bound_method)) => { let instance = bound_method.self_instance(db); let arguments = arguments.with_self(instance); - let binding = bind_call( db, &arguments, bound_method.function(db).signature(db), self, ); - - if binding.has_binding_errors() { - Err(CallError::BindingError { binding }) - } else { - Ok(CallOutcome::Single(binding)) - } + binding.into_outcome() } Type::Callable(CallableType::MethodWrapperDunderGet(function)) => { // Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`. @@ -1965,13 +1959,7 @@ impl<'db> Type<'db> { }, ); - let binding = bind_call(db, arguments, &signature, self); - - if binding.has_binding_errors() { - Err(CallError::BindingError { binding }) - } else { - Ok(CallOutcome::Single(binding)) - } + bind_call(db, arguments, &signature, self).into_outcome() } Type::Callable(CallableType::WrapperDescriptorDunderGet) => { // Here, we also model `types.FunctionType.__get__`, but now we consider a call to @@ -2050,13 +2038,7 @@ impl<'db> Type<'db> { ), ); - let binding = bind_call(db, arguments, &signature, self); - - if binding.has_binding_errors() { - Err(CallError::BindingError { binding }) - } else { - Ok(CallOutcome::Single(binding)) - } + bind_call(db, arguments, &signature, self).into_outcome() } Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); @@ -2146,11 +2128,11 @@ impl<'db> Type<'db> { let Some((instance_ty, attr_name, default)) = binding.three_parameter_types() else { - return Ok(CallOutcome::Single(binding)); + return binding.into_outcome(); }; let Some(attr_name) = attr_name.into_string_literal() else { - return Ok(CallOutcome::Single(binding)); + return binding.into_outcome(); }; let default = if default.is_unknown() { @@ -2187,44 +2169,39 @@ impl<'db> Type<'db> { _ => {} }; - if binding.has_binding_errors() { - Err(CallError::BindingError { binding }) - } else { - Ok(CallOutcome::Single(binding)) - } + binding.into_outcome() } // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { class }) => { - Ok(CallOutcome::Single(CallBinding::from_return_type( - match class.known(db) { - // TODO: We should check the call signature and error if the bool call doesn't have the - // right signature and return a binding error. - - // If the class is the builtin-bool class (for example `bool(1)`), we try to - // return the specific truthiness value of the input arg, `Literal[True]` for - // the example above. - Some(KnownClass::Bool) => arguments - .first_argument() - .map(|arg| arg.bool(db).into_type(db)) - .unwrap_or(Type::BooleanLiteral(false)), - - // TODO: Don't ignore the second and third arguments to `str` - // https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568 - Some(KnownClass::Str) => arguments - .first_argument() - .map(|arg| arg.str(db)) - .unwrap_or_else(|| Type::string_literal(db, "")), - - Some(KnownClass::Type) => arguments - .exactly_one_argument() - .map(|arg| arg.to_meta_type(db)) - .unwrap_or_else(|| KnownClass::Type.to_instance(db)), - - _ => Type::Instance(InstanceType { class }), - }, - ))) + let binding = CallBinding::from_return_type(match class.known(db) { + // TODO: We should check the call signature and error if the bool call doesn't have the + // right signature and return a binding error. + + // If the class is the builtin-bool class (for example `bool(1)`), we try to + // return the specific truthiness value of the input arg, `Literal[True]` for + // the example above. + Some(KnownClass::Bool) => arguments + .first_argument() + .map(|arg| arg.bool(db).into_type(db)) + .unwrap_or(Type::BooleanLiteral(false)), + + // TODO: Don't ignore the second and third arguments to `str` + // https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568 + Some(KnownClass::Str) => arguments + .first_argument() + .map(|arg| arg.str(db)) + .unwrap_or_else(|| Type::string_literal(db, "")), + + Some(KnownClass::Type) => arguments + .exactly_one_argument() + .map(|arg| arg.to_meta_type(db)) + .unwrap_or_else(|| KnownClass::Type.to_instance(db)), + + _ => Type::Instance(InstanceType { class }), + }); + binding.into_outcome() } instance_ty @ Type::Instance(_) => { @@ -2267,17 +2244,16 @@ impl<'db> Type<'db> { // Dynamic types are callable, and the return type is the same dynamic type. Similarly, // `Never` is always callable and returns `Never`. - Type::Dynamic(_) | Type::Never => { - Ok(CallOutcome::Single(CallBinding::from_return_type(self))) - } + Type::Dynamic(_) | Type::Never => CallBinding::from_return_type(self).into_outcome(), Type::Union(union) => { CallOutcome::try_call_union(db, union, |element| element.try_call(db, arguments)) } - Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type( - todo_type!("Type::Intersection.call()"), - ))), + Type::Intersection(_) => { + CallBinding::from_return_type(todo_type!("Type::Intersection.call()")) + .into_outcome() + } _ => Err(CallError::NotCallable { not_callable_type: self, diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 208a3e46e1b21..6588d7b1c27df 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -1,4 +1,4 @@ -use super::{Argument, CallArguments, InferContext, Signature, Type}; +use super::{Argument, CallArguments, CallError, CallOutcome, InferContext, Signature, Type}; use crate::db::Db; use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED, @@ -170,6 +170,13 @@ impl<'db> CallBinding<'db> { } } + pub(crate) fn into_outcome(self) -> Result, CallError<'db>> { + if self.has_binding_errors() { + return Err(CallError::BindingError { binding: self }); + } + Ok(CallOutcome::Single(self)) + } + pub(crate) fn callable_type(&self) -> Type<'db> { self.callable_ty } From 5221ffda8eed6a5d7a1c75dd500ce5e0b71b9b0b Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 09:07:41 -0500 Subject: [PATCH 02/32] Add separate bindings for each overload --- crates/red_knot_python_semantic/src/types.rs | 69 +++---- .../src/types/call.rs | 10 +- .../src/types/call/bind.rs | 173 +++++++++++++----- .../src/types/class.rs | 4 +- .../src/types/infer.rs | 10 +- 5 files changed, 178 insertions(+), 88 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f97884d3fe794..96db17c3d7e7f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -29,7 +29,7 @@ use crate::semantic_index::symbol::ScopeId; use crate::semantic_index::{imported_modules, semantic_index}; use crate::suppression::check_suppressions; use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; -use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome, UnionCallError}; +use crate::types::call::{bind_call, CallArguments, CallOutcome, OverloadBinding, UnionCallError}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; use crate::types::infer::infer_unpack_types; @@ -1751,7 +1751,7 @@ impl<'db> Type<'db> { match err { CallError::BindingError { binding } => { return Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(binding.return_type()), + truthiness: type_to_truthiness(binding.return_type(db)), not_boolable_type: *instance_ty, }); } @@ -2043,70 +2043,71 @@ impl<'db> Type<'db> { Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); - if binding.has_binding_errors() { + let Some(overload) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); - } + }; match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { - let (ty_a, ty_b) = binding + let (ty_a, ty_b) = overload .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); - binding + overload .set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b))); } Some(KnownFunction::IsSubtypeOf) => { - let (ty_a, ty_b) = binding + let (ty_a, ty_b) = overload .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); - binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b))); + overload + .set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b))); } Some(KnownFunction::IsAssignableTo) => { - let (ty_a, ty_b) = binding + let (ty_a, ty_b) = overload .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); - binding + overload .set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b))); } Some(KnownFunction::IsDisjointFrom) => { - let (ty_a, ty_b) = binding + let (ty_a, ty_b) = overload .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); - binding + overload .set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b))); } Some(KnownFunction::IsGradualEquivalentTo) => { - let (ty_a, ty_b) = binding + let (ty_a, ty_b) = overload .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); - binding.set_return_type(Type::BooleanLiteral( + overload.set_return_type(Type::BooleanLiteral( ty_a.is_gradual_equivalent_to(db, ty_b), )); } Some(KnownFunction::IsFullyStatic) => { - let ty = binding.one_parameter_type().unwrap_or(Type::unknown()); - binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); + let ty = overload.one_parameter_type().unwrap_or(Type::unknown()); + overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); } Some(KnownFunction::IsSingleton) => { - let ty = binding.one_parameter_type().unwrap_or(Type::unknown()); - binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); + let ty = overload.one_parameter_type().unwrap_or(Type::unknown()); + overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); } Some(KnownFunction::IsSingleValued) => { - let ty = binding.one_parameter_type().unwrap_or(Type::unknown()); - binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); + let ty = overload.one_parameter_type().unwrap_or(Type::unknown()); + overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); } Some(KnownFunction::Len) => { - if let Some(first_arg) = binding.one_parameter_type() { + if let Some(first_arg) = overload.one_parameter_type() { if let Some(len_ty) = first_arg.len(db) { - binding.set_return_type(len_ty); + overload.set_return_type(len_ty); } }; } Some(KnownFunction::Repr) => { - if let Some(first_arg) = binding.one_parameter_type() { - binding.set_return_type(first_arg.repr(db)); + if let Some(first_arg) = overload.one_parameter_type() { + overload.set_return_type(first_arg.repr(db)); }; } @@ -2114,19 +2115,19 @@ impl<'db> Type<'db> { // TODO: Use `.two_parameter_tys()` exclusively // when overloads are supported. if let Some(casted_ty) = arguments.first_argument() { - if binding.two_parameter_types().is_some() { - binding.set_return_type(casted_ty); + if overload.two_parameter_types().is_some() { + overload.set_return_type(casted_ty); } }; } Some(KnownFunction::Overload) => { - binding.set_return_type(todo_type!("overload(..) return type")); + overload.set_return_type(todo_type!("overload(..) return type")); } Some(KnownFunction::GetattrStatic) => { let Some((instance_ty, attr_name, default)) = - binding.three_parameter_types() + overload.three_parameter_types() else { return binding.into_outcome(); }; @@ -2144,7 +2145,7 @@ impl<'db> Type<'db> { let union_with_default = |ty| UnionType::from_elements(db, [ty, default]); // TODO: we could emit a diagnostic here (if default is not set) - binding.set_return_type( + overload.set_return_type( match instance_ty.static_member(db, attr_name.value(db)) { Symbol::Type(ty, Boundness::Bound) => { if instance_ty.is_fully_static(db) { @@ -2175,7 +2176,7 @@ impl<'db> Type<'db> { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { class }) => { - let binding = CallBinding::from_return_type(match class.known(db) { + let overload = OverloadBinding::from_return_type(match class.known(db) { // TODO: We should check the call signature and error if the bool call doesn't have the // right signature and return a binding error. @@ -2201,6 +2202,7 @@ impl<'db> Type<'db> { _ => Type::Instance(InstanceType { class }), }); + let binding = overload.into_binding(); binding.into_outcome() } @@ -2244,14 +2246,17 @@ impl<'db> Type<'db> { // Dynamic types are callable, and the return type is the same dynamic type. Similarly, // `Never` is always callable and returns `Never`. - Type::Dynamic(_) | Type::Never => CallBinding::from_return_type(self).into_outcome(), + Type::Dynamic(_) | Type::Never => OverloadBinding::from_return_type(self) + .into_binding() + .into_outcome(), Type::Union(union) => { CallOutcome::try_call_union(db, union, |element| element.try_call(db, arguments)) } Type::Intersection(_) => { - CallBinding::from_return_type(todo_type!("Type::Intersection.call()")) + OverloadBinding::from_return_type(todo_type!("Type::Intersection.call()")) + .into_binding() .into_outcome() } diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index 0fd81de9f4975..9272d27954ec8 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -6,7 +6,7 @@ use crate::Db; mod arguments; mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{bind_call, CallBinding}; +pub(super) use bind::{bind_call, CallBinding, OverloadBinding}; /// A successfully bound call where all arguments are valid. /// @@ -68,9 +68,9 @@ impl<'db> CallOutcome<'db> { /// The type returned by this call. pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { match self { - Self::Single(binding) => binding.return_type(), + Self::Single(binding) => binding.return_type(db), Self::Union(bindings) => { - UnionType::from_elements(db, bindings.iter().map(bind::CallBinding::return_type)) + UnionType::from_elements(db, bindings.iter().map(|binding| binding.return_type(db))) } } } @@ -123,11 +123,11 @@ impl<'db> CallError<'db> { db, bindings .iter() - .map(CallBinding::return_type) + .map(|binding| binding.return_type(db)) .chain(errors.iter().map(|err| err.fallback_return_type(db))), )), Self::PossiblyUnboundDunderCall { outcome, .. } => Some(outcome.return_type(db)), - Self::BindingError { binding } => Some(binding.return_type()), + Self::BindingError { binding } => Some(binding.return_type(db)), } } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 6588d7b1c27df..d979ab7112304 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -126,14 +126,18 @@ pub(crate) fn bind_call<'db>( }); } - CallBinding { - callable_ty, + let overload = OverloadBinding { return_ty: signature.return_ty.unwrap_or(Type::unknown()), parameter_tys: parameter_tys .into_iter() .map(|opt_ty| opt_ty.unwrap_or(Type::unknown())) .collect(), errors, + }; + + CallBinding { + callable_ty, + overloads: vec![overload].into_boxed_slice(), } } @@ -144,32 +148,24 @@ pub(crate) struct CallableDescriptor<'a> { kind: &'a str, } +/// Binding information for a call site. +/// +/// For a successful binding, each argument is mapped to one of the callable's formal parameters. +/// If the callable has multiple overloads, the first one that matches is used as the overall +/// binding match. +/// +/// If the arguments cannot be matched to formal parameters, we store information about the +/// specific errors that occurred when trying to match them up. If the callable has multiple +/// overloads, we store this error information for each overload. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct CallBinding<'db> { /// Type of the callable object (function, class...) callable_ty: Type<'db>, - /// Return type of the call. - return_ty: Type<'db>, - - /// Bound types for parameters, in parameter source order. - parameter_tys: Box<[Type<'db>]>, - - /// Call binding errors, if any. - errors: Vec>, + overloads: Box<[OverloadBinding<'db>]>, } impl<'db> CallBinding<'db> { - // TODO remove this constructor and construct always from `bind_call` - pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self { - Self { - callable_ty: todo_type!("CallBinding::from_return_type"), - return_ty, - parameter_tys: Box::default(), - errors: vec![], - } - } - pub(crate) fn into_outcome(self) -> Result, CallError<'db>> { if self.has_binding_errors() { return Err(CallError::BindingError { binding: self }); @@ -181,37 +177,40 @@ impl<'db> CallBinding<'db> { self.callable_ty } - pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { - self.return_ty = return_ty; - } - - pub(crate) fn return_type(&self) -> Type<'db> { - self.return_ty + /// Returns whether there were any errors binding this call site. If the callable has multiple + /// overloads, they must _all_ have errors. + pub(crate) fn has_binding_errors(&self) -> bool { + self.overloads + .iter() + .all(OverloadBinding::has_binding_errors) } - pub(crate) fn parameter_types(&self) -> &[Type<'db>] { - &self.parameter_tys + /// Returns the overload that matched for this call binding. Returns `None` if none of the + /// overloads matched. + pub(crate) fn matching_overload(&self) -> Option<&OverloadBinding<'db>> { + self.overloads + .iter() + .find(|overload| !overload.has_binding_errors()) } - pub(crate) fn one_parameter_type(&self) -> Option> { - match self.parameter_types() { - [ty] => Some(*ty), - _ => None, - } + /// Returns the overload that matched for this call binding. Returns `None` if none of the + /// overloads matched. + pub(crate) fn matching_overload_mut(&mut self) -> Option<&mut OverloadBinding<'db>> { + self.overloads + .iter_mut() + .find(|overload| !overload.has_binding_errors()) } - pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> { - match self.parameter_types() { - [first, second] => Some((*first, *second)), - _ => None, + /// Returns the return type of the matching overload for this binding. If none of the overloads + /// matched, returns a union of the return types of each overload. + pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + if let Some(overload) = self.matching_overload() { + return overload.return_type(); } - } - - pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> { - match self.parameter_types() { - [first, second, third] => Some((*first, *second, *third)), - _ => None, + if let [overload] = self.overloads.as_ref() { + return overload.return_type(); } + UnionType::from_elements(db, self.overloads.iter().map(OverloadBinding::return_type)) } fn callable_descriptor(&self, db: &'db dyn Db) -> Option { @@ -242,10 +241,15 @@ impl<'db> CallBinding<'db> { } } + /// Report diagnostics for all of the errors that occurred when trying to match actual + /// arguments to formal parameters. If the callable has multiple overloads, we only report + /// diagnostics for the first overload. pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { - let callable_descriptor = self.callable_descriptor(context.db()); - for error in &self.errors { - error.report_diagnostic( + // TODO: We currently only report the errors from the first overload. Consider reporting + // errors from all of them. + if let Some(overload) = self.overloads.first() { + let callable_descriptor = self.callable_descriptor(context.db()); + overload.report_diagnostics( context, node, self.callable_ty, @@ -253,6 +257,83 @@ impl<'db> CallBinding<'db> { ); } } +} + +/// Binding information for one of the overloads of a callable. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct OverloadBinding<'db> { + /// Return type of the call. + return_ty: Type<'db>, + + /// Bound types for parameters, in parameter source order. + parameter_tys: Box<[Type<'db>]>, + + /// Call binding errors, if any. + errors: Vec>, +} + +impl<'db> OverloadBinding<'db> { + // TODO remove this constructor and construct always from `bind_call` + pub(crate) fn into_binding(self) -> CallBinding<'db> { + CallBinding { + callable_ty: todo_type!("CallBinding::from_return_type"), + overloads: vec![self].into_boxed_slice(), + } + } + + // TODO remove this constructor and construct always from `bind_call` + pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self { + Self { + return_ty, + parameter_tys: Box::default(), + errors: vec![], + } + } + + pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { + self.return_ty = return_ty; + } + + pub(crate) fn return_type(&self) -> Type<'db> { + self.return_ty + } + + pub(crate) fn parameter_types(&self) -> &[Type<'db>] { + &self.parameter_tys + } + + pub(crate) fn one_parameter_type(&self) -> Option> { + match self.parameter_types() { + [ty] => Some(*ty), + _ => None, + } + } + + pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> { + match self.parameter_types() { + [first, second] => Some((*first, *second)), + _ => None, + } + } + + pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> { + match self.parameter_types() { + [first, second, third] => Some((*first, *second, *third)), + _ => None, + } + } + + fn report_diagnostics( + &self, + context: &InferContext<'db>, + node: ast::AnyNodeRef, + callable_ty: Type<'db>, + callable_descriptor: Option<&CallableDescriptor>, + ) { + for error in &self.errors { + error.report_diagnostic(context, node, callable_ty, callable_descriptor); + } + } pub(crate) fn has_binding_errors(&self) -> bool { !self.errors.is_empty() diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e61fdf43e7b7f..616385ae07d3a 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -251,7 +251,7 @@ impl<'db> Class<'db> { }) .map(|mut builder| { for binding in bindings { - builder = builder.add(binding.return_type()); + builder = builder.add(binding.return_type(db)); } builder.build() @@ -272,7 +272,7 @@ impl<'db> Class<'db> { // TODO we should also check for binding errors that would indicate the metaclass // does not accept the right arguments - Err(CallError::BindingError { binding }) => Ok(binding.return_type()), + Err(CallError::BindingError { binding }) => Ok(binding.return_type(db)), }; return return_ty_result.map(|ty| ty.to_meta_type(db)); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index bf8e4c1d23a0f..856329bca1dd5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3423,9 +3423,13 @@ impl<'db> TypeInferenceBuilder<'db> { continue; }; + let Some(overload) = binding.matching_overload() else { + continue; + }; + match known_function { KnownFunction::RevealType => { - if let Some(revealed_type) = binding.one_parameter_type() { + if let Some(revealed_type) = overload.one_parameter_type() { self.context.report_diagnostic( call_expression, DiagnosticId::RevealedType, @@ -3439,7 +3443,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } KnownFunction::AssertType => { - if let [actual_ty, asserted_ty] = binding.parameter_types() { + if let [actual_ty, asserted_ty] = overload.parameter_types() { if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) { self.context.report_lint( &TYPE_ASSERTION_FAILURE, @@ -3454,7 +3458,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } KnownFunction::StaticAssert => { - if let Some((parameter_ty, message)) = binding.two_parameter_types() { + if let Some((parameter_ty, message)) = overload.two_parameter_types() { let truthiness = match parameter_ty.try_bool(self.db()) { Ok(truthiness) => truthiness, Err(err) => { From 4329cac87c9852bf2b291b97478475a6e4676258 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 09:35:38 -0500 Subject: [PATCH 03/32] Report diagnostics for all overloads --- crates/red_knot_python_semantic/src/types/call/bind.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index d979ab7112304..2c2228329ea31 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -242,13 +242,11 @@ impl<'db> CallBinding<'db> { } /// Report diagnostics for all of the errors that occurred when trying to match actual - /// arguments to formal parameters. If the callable has multiple overloads, we only report - /// diagnostics for the first overload. + /// arguments to formal parameters. If the callable has multiple overloads, we report + /// diagnostics for all of them. pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { - // TODO: We currently only report the errors from the first overload. Consider reporting - // errors from all of them. - if let Some(overload) = self.overloads.first() { - let callable_descriptor = self.callable_descriptor(context.db()); + let callable_descriptor = self.callable_descriptor(context.db()); + for overload in &self.overloads { overload.report_diagnostics( context, node, From 184154f3487c68d94ed9b2f0b816fce27ae6177e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 09:48:23 -0500 Subject: [PATCH 04/32] Add overloads of signatures --- crates/red_knot_python_semantic/src/types.rs | 12 +++--- .../src/types/call.rs | 2 +- .../src/types/call/bind.rs | 29 +++++++++---- .../src/types/signatures.rs | 41 +++++++++++++++++-- 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 96db17c3d7e7f..3187eefc6f5f4 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -19,7 +19,7 @@ pub(crate) use self::infer::{ infer_scope_types, }; pub use self::narrow::KnownConstraintFunction; -pub(crate) use self::signatures::Signature; +pub(crate) use self::signatures::{Overloads, Signature}; pub use self::subclass_of::SubclassOfType; use crate::module_name::ModuleName; use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; @@ -1959,7 +1959,7 @@ impl<'db> Type<'db> { }, ); - bind_call(db, arguments, &signature, self).into_outcome() + bind_call(db, arguments, &signature.into(), self).into_outcome() } Type::Callable(CallableType::WrapperDescriptorDunderGet) => { // Here, we also model `types.FunctionType.__get__`, but now we consider a call to @@ -2038,7 +2038,7 @@ impl<'db> Type<'db> { ), ); - bind_call(db, arguments, &signature, self).into_outcome() + bind_call(db, arguments, &signature.into(), self).into_outcome() } Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); @@ -3512,8 +3512,8 @@ impl<'db> FunctionType<'db> { /// Were this not a salsa query, then the calling query /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(return_ref)] - pub fn signature(self, db: &'db dyn Db) -> Signature<'db> { - let internal_signature = self.internal_signature(db); + pub fn signature(self, db: &'db dyn Db) -> Overloads<'db> { + let internal_signature = self.internal_signature(db).into(); let decorators = self.decorators(db); let mut decorators = decorators.iter(); @@ -3525,7 +3525,7 @@ impl<'db> FunctionType<'db> { { internal_signature } else { - Signature::todo("return type of decorated function") + Signature::todo("return type of decorated function").into() } } else { internal_signature diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index 9272d27954ec8..c5ac53901f0b8 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -1,5 +1,5 @@ use super::context::InferContext; -use super::{Signature, Type}; +use super::{Overloads, Signature, Type}; use crate::types::UnionType; use crate::Db; diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 2c2228329ea31..f36e65f89147d 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -1,4 +1,6 @@ -use super::{Argument, CallArguments, CallError, CallOutcome, InferContext, Signature, Type}; +use super::{ + Argument, CallArguments, CallError, CallOutcome, InferContext, Overloads, Signature, Type, +}; use crate::db::Db; use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED, @@ -17,9 +19,25 @@ use ruff_text_size::Ranged; pub(crate) fn bind_call<'db>( db: &'db dyn Db, arguments: &CallArguments<'_, 'db>, - signature: &Signature<'db>, + overloads: &Overloads<'db>, callable_ty: Type<'db>, ) -> CallBinding<'db> { + let overloads = overloads + .iter() + .map(|signature| bind_overload(db, arguments, signature)) + .collect::>() + .into_boxed_slice(); + CallBinding { + callable_ty, + overloads, + } +} + +fn bind_overload<'db>( + db: &'db dyn Db, + arguments: &CallArguments<'_, 'db>, + signature: &Signature<'db>, +) -> OverloadBinding<'db> { let parameters = signature.parameters(); // The type assigned to each parameter at this call site. let mut parameter_tys = vec![None; parameters.len()]; @@ -126,18 +144,13 @@ pub(crate) fn bind_call<'db>( }); } - let overload = OverloadBinding { + OverloadBinding { return_ty: signature.return_ty.unwrap_or(Type::unknown()), parameter_tys: parameter_tys .into_iter() .map(|opt_ty| opt_ty.unwrap_or(Type::unknown())) .collect(), errors, - }; - - CallBinding { - callable_ty, - overloads: vec![overload].into_boxed_slice(), } } diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 17c4f57c73ab3..9e885c970af27 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -3,7 +3,42 @@ use crate::Db; use crate::{semantic_index::definition::Definition, types::todo_type}; use ruff_python_ast::{self as ast, name::Name}; -/// A typed callable signature. +/// A collection of overloads for a callable. +#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)] +pub(crate) enum Overloads<'db> { + Single(Signature<'db>), + Overloaded(Box<[Signature<'db>]>), +} + +impl<'db> Overloads<'db> { + pub(crate) fn iter(&self) -> std::slice::Iter> { + match self { + Overloads::Single(signature) => std::slice::from_ref(signature).iter(), + Overloads::Overloaded(signatures) => signatures.iter(), + } + } +} + +impl<'db> From> for Overloads<'db> { + fn from(signature: Signature<'db>) -> Self { + Overloads::Single(signature) + } +} + +impl<'db, I> From for Overloads<'db> +where + I: IntoIterator, + I::IntoIter: Iterator>, +{ + fn from(overloads: I) -> Self { + let overloads = overloads.into_iter().collect::>().into_boxed_slice(); + debug_assert!(!overloads.is_empty()); + Overloads::Overloaded(overloads) + } +} + +/// A typed callable signature. If a callable is overloaded, there will be one of these for each +/// possible overload. #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)] pub(crate) struct Signature<'db> { /// Parameters, in source order. @@ -630,7 +665,7 @@ mod tests { .unwrap(); let func = get_function_f(&db, "/src/a.py"); - let expected_sig = func.internal_signature(&db); + let expected_sig = func.internal_signature(&db).into(); // With no decorators, internal and external signature are the same assert_eq!(func.signature(&db), &expected_sig); @@ -651,7 +686,7 @@ mod tests { .unwrap(); let func = get_function_f(&db, "/src/a.py"); - let expected_sig = Signature::todo("return type of decorated function"); + let expected_sig = Signature::todo("return type of decorated function").into(); // With no decorators, internal and external signature are the same assert_eq!(func.signature(&db), &expected_sig); From f85734d665e402e91b4feaab3b1c8b815b0ea4b0 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 10:30:38 -0500 Subject: [PATCH 05/32] Use overloads for decorator method special cases --- .../resources/mdtest/call/methods.md | 11 +- .../resources/mdtest/descriptor_protocol.md | 15 +- crates/red_knot_python_semantic/src/types.rs | 220 ++++++++++-------- .../src/types/signatures.rs | 27 ++- 4 files changed, 158 insertions(+), 115 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 32b248b8a56d7..4df6a8c19f33c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -235,22 +235,29 @@ method_wrapper(C(), None) method_wrapper(None, C) # Passing `None` without an `owner` argument is an -# error: [missing-argument] "No argument provided for required parameter `owner`" +# error: [missing-argument] "No argument provided for required parameter `owner` of method wrapper `__get__` of function `f`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `~None`" method_wrapper(None) # Passing something that is not assignable to `type` as the `owner` argument is an # error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `~None`" +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type | None`" method_wrapper(None, 1) # Passing `None` as the `owner` argument when `instance` is `None` is an # error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `~None`" method_wrapper(None, None) # Calling `__get__` without any arguments is an -# error: [missing-argument] "No argument provided for required parameter `instance`" +# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of method wrapper `__get__` of function `f`" +# error: [missing-argument] "No argument provided for required parameter `instance` of method wrapper `__get__` of function `f`" method_wrapper() # Calling `__get__` with too many positional arguments is an +# error: [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `None`" +# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3" # error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3" method_wrapper(C(), C, "one too many") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index 7e6407fa45afd..e3d49bcc9ae84 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -410,15 +410,18 @@ Finally, we test some error cases for the call to the wrapper descriptor: ```py # Calling the wrapper descriptor without any arguments is an -# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`" +# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`, `owner` of wrapper descriptor `FunctionType.__get__`" +# error: [missing-argument] "No arguments provided for required parameters `self`, `instance` of wrapper descriptor `FunctionType.__get__`" wrapper_descriptor() # Calling it without the `instance` argument is an also an -# error: [missing-argument] "No argument provided for required parameter `instance`" +# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of wrapper descriptor `FunctionType.__get__`" +# error: [missing-argument] "No argument provided for required parameter `instance` of wrapper descriptor `FunctionType.__get__`" wrapper_descriptor(f) # Calling it without the `owner` argument if `instance` is not `None` is an -# error: [missing-argument] "No argument provided for required parameter `owner`" +# error: [missing-argument] "No argument provided for required parameter `owner` of wrapper descriptor `FunctionType.__get__`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" wrapper_descriptor(f, None) # But calling it with an instance is fine (in this case, the `owner` argument is optional): @@ -426,14 +429,20 @@ wrapper_descriptor(f, C()) # Calling it with something that is not a `FunctionType` as the first argument is an # error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" wrapper_descriptor(1, None, type(f)) # Calling it with something that is not a `type` as the `owner` argument is an # error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type | None`" wrapper_descriptor(f, None, f) # Calling it with too many positional arguments is an # error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4" wrapper_descriptor(f, None, type(f), "one too many") ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3187eefc6f5f4..8c2e36ab6fadb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1905,23 +1905,30 @@ impl<'db> Type<'db> { // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... // ``` - let first_argument_is_none = - arguments.first_argument().is_some_and(|ty| ty.is_none(db)); - - let signature = Signature::new( - Parameters::new([ - Parameter::new( - Some("instance".into()), - Some(Type::object(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - if first_argument_is_none { + let not_none = Type::none(db).negate(db); + let overloads = Overloads::from_overloads([ + Signature::new( + Parameters::new([ + Parameter::new( + Some("instance".into()), + Some(Type::none(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), Parameter::new( Some("owner".into()), Some(KnownClass::Type.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, - ) - } else { + ), + ]), + None, + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some("instance".into()), + Some(not_none), + ParameterKind::PositionalOnly { default_ty: None }, + ), Parameter::new( Some("owner".into()), Some(UnionType::from_elements( @@ -1931,62 +1938,81 @@ impl<'db> Type<'db> { ParameterKind::PositionalOnly { default_ty: Some(Type::none(db)), }, - ) - }, - ]), - if function.has_known_class_decorator(db, KnownClass::Classmethod) - && function.decorators(db).len() == 1 - { - if let Some(owner) = arguments.second_argument() { - Some(Type::Callable(CallableType::BoundMethod( - BoundMethodType::new(db, function, owner), - ))) - } else if let Some(instance) = arguments.first_argument() { - Some(Type::Callable(CallableType::BoundMethod( - BoundMethodType::new(db, function, instance.to_meta_type(db)), - ))) + ), + ]), + None, + ), + ]); + + let mut binding = bind_call(db, arguments, &overloads, self); + let Some(overload) = binding.matching_overload_mut() else { + return Err(CallError::BindingError { binding }); + }; + + if function.has_known_class_decorator(db, KnownClass::Classmethod) + && function.decorators(db).len() == 1 + { + if let Some(owner) = arguments.second_argument() { + overload.set_return_type(Type::Callable(CallableType::BoundMethod( + BoundMethodType::new(db, function, owner), + ))); + } else if let Some(instance) = arguments.first_argument() { + overload.set_return_type(Type::Callable(CallableType::BoundMethod( + BoundMethodType::new(db, function, instance.to_meta_type(db)), + ))); + } + } else { + if let Some(first) = arguments.first_argument() { + if first.is_none(db) { + overload.set_return_type(Type::FunctionLiteral(function)); } else { - Some(Type::unknown()) + overload.set_return_type(Type::Callable(CallableType::BoundMethod( + BoundMethodType::new(db, function, first), + ))); } - } else { - Some(match arguments.first_argument() { - Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function), - Some(instance) => Type::Callable(CallableType::BoundMethod( - BoundMethodType::new(db, function, instance), - )), - _ => Type::unknown(), - }) - }, - ); + } + } - bind_call(db, arguments, &signature.into(), self).into_outcome() + binding.into_outcome() } Type::Callable(CallableType::WrapperDescriptorDunderGet) => { // Here, we also model `types.FunctionType.__get__`, but now we consider a call to // this as a function, i.e. we also expect the `self` argument to be passed in. - let second_argument_is_none = - arguments.second_argument().is_some_and(|ty| ty.is_none(db)); - - let signature = Signature::new( - Parameters::new([ - Parameter::new( - Some("self".into()), - Some(KnownClass::FunctionType.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some("instance".into()), - Some(Type::object(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - if second_argument_is_none { + let not_none = Type::none(db).negate(db); + let overloads = Overloads::from_overloads([ + Signature::new( + Parameters::new([ + Parameter::new( + Some("self".into()), + Some(KnownClass::FunctionType.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("instance".into()), + Some(Type::none(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), Parameter::new( Some("owner".into()), Some(KnownClass::Type.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, - ) - } else { + ), + ]), + None, + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some("self".into()), + Some(KnownClass::FunctionType.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("instance".into()), + Some(not_none), + ParameterKind::PositionalOnly { default_ty: None }, + ), Parameter::new( Some("owner".into()), Some(UnionType::from_elements( @@ -1996,53 +2022,51 @@ impl<'db> Type<'db> { ParameterKind::PositionalOnly { default_ty: Some(Type::none(db)), }, - ) - }, - ]), - Some( - if let Some(function_ty @ Type::FunctionLiteral(function)) = - arguments.first_argument() - { - if function.has_known_class_decorator(db, KnownClass::Classmethod) - && function.decorators(db).len() == 1 - { - if let Some(owner) = arguments.third_argument() { - Type::Callable(CallableType::BoundMethod(BoundMethodType::new( - db, function, owner, - ))) - } else if let Some(instance) = arguments.second_argument() { - Type::Callable(CallableType::BoundMethod(BoundMethodType::new( - db, - function, - instance.to_meta_type(db), - ))) - } else { - Type::unknown() - } + ), + ]), + None, + ), + ]); + + let mut binding = bind_call(db, arguments, &overloads, self); + let Some(overload) = binding.matching_overload_mut() else { + return Err(CallError::BindingError { binding }); + }; + + if let Some(function_ty @ Type::FunctionLiteral(function)) = + arguments.first_argument() + { + if function.has_known_class_decorator(db, KnownClass::Classmethod) + && function.decorators(db).len() == 1 + { + if let Some(owner) = arguments.third_argument() { + overload.set_return_type(Type::Callable(CallableType::BoundMethod( + BoundMethodType::new(db, function, owner), + ))); + } else if let Some(instance) = arguments.second_argument() { + overload.set_return_type(Type::Callable(CallableType::BoundMethod( + BoundMethodType::new(db, function, instance.to_meta_type(db)), + ))); + } + } else { + if let Some(instance) = arguments.second_argument() { + if instance.is_none(db) { + overload.set_return_type(function_ty); } else { - if let Some(instance) = arguments.second_argument() { - if instance.is_none(db) { - function_ty - } else { - Type::Callable(CallableType::BoundMethod( - BoundMethodType::new(db, function, instance), - )) - } - } else { - Type::unknown() - } + overload.set_return_type(Type::Callable( + CallableType::BoundMethod(BoundMethodType::new( + db, function, instance, + )), + )); } - } else { - Type::unknown() - }, - ), - ); + } + } + } - bind_call(db, arguments, &signature.into(), self).into_outcome() + binding.into_outcome() } Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); - let Some(overload) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 9e885c970af27..9f65fcb65eaa2 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -11,6 +11,21 @@ pub(crate) enum Overloads<'db> { } impl<'db> Overloads<'db> { + pub(crate) fn from_overloads(overloads: I) -> Self + where + I: IntoIterator, + I::IntoIter: Iterator>, + { + let mut iter = overloads.into_iter(); + let first_overload = iter.next().expect("overloads should not be empty"); + let Some(second_overload) = iter.next() else { + return Overloads::Single(first_overload); + }; + let mut overloads = vec![first_overload, second_overload]; + overloads.extend(iter); + Overloads::Overloaded(overloads.into()) + } + pub(crate) fn iter(&self) -> std::slice::Iter> { match self { Overloads::Single(signature) => std::slice::from_ref(signature).iter(), @@ -25,18 +40,6 @@ impl<'db> From> for Overloads<'db> { } } -impl<'db, I> From for Overloads<'db> -where - I: IntoIterator, - I::IntoIter: Iterator>, -{ - fn from(overloads: I) -> Self { - let overloads = overloads.into_iter().collect::>().into_boxed_slice(); - debug_assert!(!overloads.is_empty()); - Overloads::Overloaded(overloads) - } -} - /// A typed callable signature. If a callable is overloaded, there will be one of these for each /// possible overload. #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)] From 257b5067b18901bdd0b4448c93031c3836cafdbe Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 10:45:22 -0500 Subject: [PATCH 06/32] Include overload number in diagnostics --- .../resources/mdtest/call/methods.md | 24 ++++----- .../resources/mdtest/descriptor_protocol.md | 30 +++++------ .../src/types/call/bind.rs | 50 ++++++++++++++++--- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 4df6a8c19f33c..c87523af1468e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -235,30 +235,30 @@ method_wrapper(C(), None) method_wrapper(None, C) # Passing `None` without an `owner` argument is an -# error: [missing-argument] "No argument provided for required parameter `owner` of method wrapper `__get__` of function `f`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `~None`" +# error: [missing-argument] "No argument provided for required parameter `owner` of overload 1 of method wrapper `__get__` of function `f`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of overload 2 of method wrapper `__get__` of function `f`; expected type `~None`" method_wrapper(None) # Passing something that is not assignable to `type` as the `owner` argument is an -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `~None`" -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type | None`" +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of overload 1 of method wrapper `__get__` of function `f`; expected type `type`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of overload 2 of method wrapper `__get__` of function `f`; expected type `~None`" +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of overload 2 of method wrapper `__get__` of function `f`; expected type `type | None`" method_wrapper(None, 1) # Passing `None` as the `owner` argument when `instance` is `None` is an -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `~None`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of overload 1 of method wrapper `__get__` of function `f`; expected type `type`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of overload 2 of method wrapper `__get__` of function `f`; expected type `~None`" method_wrapper(None, None) # Calling `__get__` without any arguments is an -# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of method wrapper `__get__` of function `f`" -# error: [missing-argument] "No argument provided for required parameter `instance` of method wrapper `__get__` of function `f`" +# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of overload 1 of method wrapper `__get__` of function `f`" +# error: [missing-argument] "No argument provided for required parameter `instance` of overload 2 of method wrapper `__get__` of function `f`" method_wrapper() # Calling `__get__` with too many positional arguments is an -# error: [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`instance`) of method wrapper `__get__` of function `f`; expected type `None`" -# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3" -# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3" +# error: [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`instance`) of overload 1 of method wrapper `__get__` of function `f`; expected type `None`" +# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of method wrapper `__get__` of function `f`: expected 2, got 3" +# error: [too-many-positional-arguments] "Too many positional arguments to overload 2 of method wrapper `__get__` of function `f`: expected 2, got 3" method_wrapper(C(), C, "one too many") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index e3d49bcc9ae84..715d69fee52eb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -410,39 +410,39 @@ Finally, we test some error cases for the call to the wrapper descriptor: ```py # Calling the wrapper descriptor without any arguments is an -# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`, `owner` of wrapper descriptor `FunctionType.__get__`" -# error: [missing-argument] "No arguments provided for required parameters `self`, `instance` of wrapper descriptor `FunctionType.__get__`" +# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`, `owner` of overload 1 of wrapper descriptor `FunctionType.__get__`" +# error: [missing-argument] "No arguments provided for required parameters `self`, `instance` of overload 2 of wrapper descriptor `FunctionType.__get__`" wrapper_descriptor() # Calling it without the `instance` argument is an also an -# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of wrapper descriptor `FunctionType.__get__`" -# error: [missing-argument] "No argument provided for required parameter `instance` of wrapper descriptor `FunctionType.__get__`" +# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of overload 1 of wrapper descriptor `FunctionType.__get__`" +# error: [missing-argument] "No argument provided for required parameter `instance` of overload 2 of wrapper descriptor `FunctionType.__get__`" wrapper_descriptor(f) # Calling it without the `owner` argument if `instance` is not `None` is an -# error: [missing-argument] "No argument provided for required parameter `owner` of wrapper descriptor `FunctionType.__get__`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [missing-argument] "No argument provided for required parameter `owner` of overload 1 of wrapper descriptor `FunctionType.__get__`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" wrapper_descriptor(f, None) # But calling it with an instance is fine (in this case, the `owner` argument is optional): wrapper_descriptor(f, C()) # Calling it with something that is not a `FunctionType` as the first argument is an -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of overload 1 of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" wrapper_descriptor(1, None, type(f)) # Calling it with something that is not a `type` as the `owner` argument is an -# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" -# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type | None`" +# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of overload 1 of wrapper descriptor `FunctionType.__get__`; expected type `type`" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `type | None`" wrapper_descriptor(f, None, f) # Calling it with too many positional arguments is an -# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of wrapper descriptor `FunctionType.__get__`; expected type `~None`" -# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4" +# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of wrapper descriptor `FunctionType.__get__`: expected 3, got 4" +# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [too-many-positional-arguments] "Too many positional arguments to overload 2 of wrapper descriptor `FunctionType.__get__`: expected 3, got 4" wrapper_descriptor(f, None, type(f), "one too many") ``` diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index f36e65f89147d..72364abdbe091 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -259,12 +259,14 @@ impl<'db> CallBinding<'db> { /// diagnostics for all of them. pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { let callable_descriptor = self.callable_descriptor(context.db()); - for overload in &self.overloads { + let multiple_overloads = self.overloads.len() > 1; + for (idx, overload) in self.overloads.iter().enumerate() { overload.report_diagnostics( context, node, self.callable_ty, callable_descriptor.as_ref(), + multiple_overloads.then(|| idx + 1), ); } } @@ -340,9 +342,10 @@ impl<'db> OverloadBinding<'db> { node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_descriptor: Option<&CallableDescriptor>, + overload: Option, ) { for error in &self.errors { - error.report_diagnostic(context, node, callable_ty, callable_descriptor); + error.report_diagnostic(context, node, callable_ty, callable_descriptor, overload); } } @@ -475,6 +478,7 @@ impl<'db> CallBindingError<'db> { node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_descriptor: Option<&CallableDescriptor>, + overload: Option, ) { match self { Self::InvalidArgumentType { @@ -500,7 +504,12 @@ impl<'db> CallBindingError<'db> { Self::get_node(node, *argument_index), format_args!( "Object of type `{provided_ty_display}` cannot be assigned to \ - parameter {parameter}{}; expected type `{expected_ty_display}`", + parameter {parameter}{}{}; expected type `{expected_ty_display}`", + if let Some(overload) = overload { + format!(" of overload {overload}") + } else { + String::new() + }, if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { @@ -520,10 +529,20 @@ impl<'db> CallBindingError<'db> { &TOO_MANY_POSITIONAL_ARGUMENTS, Self::get_node(node, *first_excess_argument_index), format_args!( - "Too many positional arguments{}: expected \ + "Too many positional arguments{}{}{}: expected \ {expected_positional_count}, got {provided_positional_count}", + if overload.is_some() || callable_descriptor.is_some() { + " to" + } else { + "" + }, + if let Some(overload) = overload { + format!(" overload {overload} of") + } else { + String::new() + }, if let Some(CallableDescriptor { kind, name }) = callable_descriptor { - format!(" to {kind} `{name}`") + format!(" {kind} `{name}`") } else { String::new() } @@ -537,7 +556,12 @@ impl<'db> CallBindingError<'db> { &MISSING_ARGUMENT, node, format_args!( - "No argument{s} provided for required parameter{s} {parameters}{}", + "No argument{s} provided for required parameter{s} {parameters}{}{}", + if let Some(overload) = overload { + format!(" of overload {overload}") + } else { + String::new() + }, if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { @@ -555,7 +579,12 @@ impl<'db> CallBindingError<'db> { &UNKNOWN_ARGUMENT, Self::get_node(node, *argument_index), format_args!( - "Argument `{argument_name}` does not match any known parameter{}", + "Argument `{argument_name}` does not match any known parameter{}{}", + if let Some(overload) = overload { + format!(" of overload {overload}") + } else { + String::new() + }, if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { @@ -573,7 +602,12 @@ impl<'db> CallBindingError<'db> { &PARAMETER_ALREADY_ASSIGNED, Self::get_node(node, *argument_index), format_args!( - "Multiple values provided for parameter {parameter}{}", + "Multiple values provided for parameter {parameter}{}{}", + if let Some(overload) = overload { + format!(" of overload {overload}") + } else { + String::new() + }, if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { From 1fb02d531dd225a60cd0d78ef0dae4667cc322ad Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 11:04:59 -0500 Subject: [PATCH 07/32] Use bind_call for intersection types TODO --- crates/red_knot_python_semantic/src/types.rs | 8 +++---- .../src/types/signatures.rs | 21 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 8c2e36ab6fadb..58b03bd8c165e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2279,9 +2279,9 @@ impl<'db> Type<'db> { } Type::Intersection(_) => { - OverloadBinding::from_return_type(todo_type!("Type::Intersection.call()")) - .into_binding() - .into_outcome() + let overloads = Overloads::todo("Type::Intersection.call()"); + let binding = bind_call(db, &arguments, &overloads, self); + binding.into_outcome() } _ => Err(CallError::NotCallable { @@ -3549,7 +3549,7 @@ impl<'db> FunctionType<'db> { { internal_signature } else { - Signature::todo("return type of decorated function").into() + Overloads::todo("return type of decorated function") } } else { internal_signature diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 9f65fcb65eaa2..cc61cb237dd36 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -32,6 +32,16 @@ impl<'db> Overloads<'db> { Overloads::Overloaded(signatures) => signatures.iter(), } } + + /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo + #[allow(unused_variables)] // 'reason' only unused in debug builds + pub(crate) fn todo(reason: &'static str) -> Self { + let signature = Signature { + parameters: Parameters::todo(), + return_ty: Some(todo_type!(reason)), + }; + signature.into() + } } impl<'db> From> for Overloads<'db> { @@ -66,15 +76,6 @@ impl<'db> Signature<'db> { } } - /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo - #[allow(unused_variables)] // 'reason' only unused in debug builds - pub(crate) fn todo(reason: &'static str) -> Self { - Self { - parameters: Parameters::todo(), - return_ty: Some(todo_type!(reason)), - } - } - /// Return a typed signature from a function definition. pub(super) fn from_function( db: &'db dyn Db, @@ -689,7 +690,7 @@ mod tests { .unwrap(); let func = get_function_f(&db, "/src/a.py"); - let expected_sig = Signature::todo("return type of decorated function").into(); + let expected_sig = Overloads::todo("return type of decorated function"); // With no decorators, internal and external signature are the same assert_eq!(func.signature(&db), &expected_sig); From 9cbcdd3bfcab64f57193925f7b3c25fed4ffbd16 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 11:17:12 -0500 Subject: [PATCH 08/32] Use bind_call for bool() --- .../resources/mdtest/call/builtins.md | 2 +- .../resources/mdtest/narrow/bool-call.md | 6 ++- crates/red_knot_python_semantic/src/types.rs | 40 ++++++++++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md index 6954499a50895..3b1b5b0f0837c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md @@ -6,7 +6,7 @@ class NotBool: __bool__ = None -# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument. +# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2" bool(1, 2) # TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly. diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md index 9bf3007e91fe0..bf2886135431a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md @@ -22,11 +22,13 @@ def _(flag: bool): # invalid invocation, too many positional args reveal_type(x) # revealed: Literal[1] | None - if bool(x is not None, 5): # TODO diagnostic + # error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2" + if bool(x is not None, 5): reveal_type(x) # revealed: Literal[1] | None # invalid invocation, too many kwargs reveal_type(x) # revealed: Literal[1] | None - if bool(x is not None, y=5): # TODO diagnostic + # error: [unknown-argument] "Argument `y` does not match any known parameter of class `bool`" + if bool(x is not None, y=5): reveal_type(x) # revealed: Literal[1] | None ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 58b03bd8c165e..534efc7e1914b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2197,21 +2197,41 @@ impl<'db> Type<'db> { binding.into_outcome() } - // TODO annotated return type on `__new__` or metaclass `__call__` - // TODO check call vs signatures of `__new__` and/or `__init__` - Type::ClassLiteral(ClassLiteralType { class }) => { - let overload = OverloadBinding::from_return_type(match class.known(db) { - // TODO: We should check the call signature and error if the bool call doesn't have the - // right signature and return a binding error. + Type::ClassLiteral(ClassLiteralType { class }) + if class.is_known(db, KnownClass::Bool) => + { + // ```py + // class bool(int): + // def __new__(cls, o: object = ..., /) -> Self: ... + // ``` + let signature = Signature::new( + Parameters::new([Parameter::new( + Some("o".into()), + Some(Type::any()), + ParameterKind::PositionalOnly { + default_ty: Some(Type::BooleanLiteral(false)), + }, + )]), + Some(self.to_instance(db)), + ); - // If the class is the builtin-bool class (for example `bool(1)`), we try to - // return the specific truthiness value of the input arg, `Literal[True]` for - // the example above. - Some(KnownClass::Bool) => arguments + let mut binding = bind_call(db, arguments, &signature.into(), self); + let Some(overload) = binding.matching_overload_mut() else { + return Err(CallError::BindingError { binding }); + }; + overload.set_return_type( + arguments .first_argument() .map(|arg| arg.bool(db).into_type(db)) .unwrap_or(Type::BooleanLiteral(false)), + ); + binding.into_outcome() + } + // TODO annotated return type on `__new__` or metaclass `__call__` + // TODO check call vs signatures of `__new__` and/or `__init__` + Type::ClassLiteral(ClassLiteralType { class }) => { + let overload = OverloadBinding::from_return_type(match class.known(db) { // TODO: Don't ignore the second and third arguments to `str` // https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568 Some(KnownClass::Str) => arguments From cb2f10ccf8e56d0e4504a91287bfd212e84bf4a4 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 11:25:30 -0500 Subject: [PATCH 09/32] Use bind_call for str() --- crates/red_knot_python_semantic/src/types.rs | 73 ++++++++++++++++--- .../src/types/call/bind.rs | 12 +-- .../src/types/infer.rs | 2 +- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 534efc7e1914b..695146c4bd77f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1945,7 +1945,7 @@ impl<'db> Type<'db> { ]); let mut binding = bind_call(db, arguments, &overloads, self); - let Some(overload) = binding.matching_overload_mut() else { + let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2029,7 +2029,7 @@ impl<'db> Type<'db> { ]); let mut binding = bind_call(db, arguments, &overloads, self); - let Some(overload) = binding.matching_overload_mut() else { + let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2067,7 +2067,7 @@ impl<'db> Type<'db> { } Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); - let Some(overload) = binding.matching_overload_mut() else { + let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2216,7 +2216,7 @@ impl<'db> Type<'db> { ); let mut binding = bind_call(db, arguments, &signature.into(), self); - let Some(overload) = binding.matching_overload_mut() else { + let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; overload.set_return_type( @@ -2228,17 +2228,68 @@ impl<'db> Type<'db> { binding.into_outcome() } + Type::ClassLiteral(ClassLiteralType { class }) + if class.is_known(db, KnownClass::Str) => + { + // ```py + // class str(Sequence[str]): + // @overload + // def __new__(cls, object: object = ...) -> Self: ... + // @overload + // def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ... + // ``` + let overloads = Overloads::from_overloads([ + Signature::new( + Parameters::new([Parameter::new( + Some("o".into()), + Some(Type::any()), + ParameterKind::PositionalOnly { + default_ty: Some(Type::string_literal(db, "")), + }, + )]), + Some(self.to_instance(db)), + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some("o".into()), + Some(Type::any()), // TODO: ReadableBuffer + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("encoding".into()), + Some(KnownClass::Str.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("errors".into()), + Some(KnownClass::Str.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + ]), + Some(self.to_instance(db)), + ), + ]); + + let mut binding = bind_call(db, arguments, &overloads, self); + let Some((index, overload)) = binding.matching_overload_mut() else { + return Err(CallError::BindingError { binding }); + }; + if index == 0 { + overload.set_return_type( + arguments + .first_argument() + .map(|arg| arg.str(db)) + .unwrap_or_else(|| Type::string_literal(db, "")), + ); + } + binding.into_outcome() + } + // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { class }) => { let overload = OverloadBinding::from_return_type(match class.known(db) { - // TODO: Don't ignore the second and third arguments to `str` - // https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568 - Some(KnownClass::Str) => arguments - .first_argument() - .map(|arg| arg.str(db)) - .unwrap_or_else(|| Type::string_literal(db, "")), - Some(KnownClass::Type) => arguments .exactly_one_argument() .map(|arg| arg.to_meta_type(db)) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 72364abdbe091..4a48d7d6a8c72 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -200,24 +200,26 @@ impl<'db> CallBinding<'db> { /// Returns the overload that matched for this call binding. Returns `None` if none of the /// overloads matched. - pub(crate) fn matching_overload(&self) -> Option<&OverloadBinding<'db>> { + pub(crate) fn matching_overload(&self) -> Option<(usize, &OverloadBinding<'db>)> { self.overloads .iter() - .find(|overload| !overload.has_binding_errors()) + .enumerate() + .find(|(_, overload)| !overload.has_binding_errors()) } /// Returns the overload that matched for this call binding. Returns `None` if none of the /// overloads matched. - pub(crate) fn matching_overload_mut(&mut self) -> Option<&mut OverloadBinding<'db>> { + pub(crate) fn matching_overload_mut(&mut self) -> Option<(usize, &mut OverloadBinding<'db>)> { self.overloads .iter_mut() - .find(|overload| !overload.has_binding_errors()) + .enumerate() + .find(|(_, overload)| !overload.has_binding_errors()) } /// Returns the return type of the matching overload for this binding. If none of the overloads /// matched, returns a union of the return types of each overload. pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { - if let Some(overload) = self.matching_overload() { + if let Some((_, overload)) = self.matching_overload() { return overload.return_type(); } if let [overload] = self.overloads.as_ref() { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 856329bca1dd5..891610429112d 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3423,7 +3423,7 @@ impl<'db> TypeInferenceBuilder<'db> { continue; }; - let Some(overload) = binding.matching_overload() else { + let Some((_, overload)) = binding.matching_overload() else { continue; }; From 7b129d8b06ef431d0b5fb3a198311bffa7e201a5 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 11:36:47 -0500 Subject: [PATCH 10/32] Use bind_call for type() --- .../resources/mdtest/call/builtins.md | 8 ++- .../resources/mdtest/narrow/type.md | 5 +- crates/red_knot_python_semantic/src/types.rs | 63 ++++++++++++++++--- .../src/types/call/arguments.rs | 8 --- 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md index 3b1b5b0f0837c..145d3bf7fb5bd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md @@ -29,9 +29,15 @@ But a three-argument call to type creates a dynamic instance of the `type` class reveal_type(type("Foo", (), {})) # revealed: type ``` -Other numbers of arguments are invalid (TODO -- these should emit a diagnostic) +Other numbers of arguments are invalid ```py +# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of class `type`: expected 1, got 2" +# error: [missing-argument] "No argument provided for required parameter `dict` of overload 2 of class `type`" type("Foo", ()) + +# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of class `type`: expected 1, got 3" +# error: [unknown-argument] "Argument `weird_other_arg` does not match any known parameter of overload 1 of class `type`" +# error: [unknown-argument] "Argument `weird_other_arg` does not match any known parameter of overload 2 of class `type`" type("Foo", (), {}, weird_other_arg=42) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index a0fdd5ca1d9ab..579f5c574f8f8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -88,7 +88,10 @@ def _(x: str | int): ```py def _(x: str | int): - # TODO: we could issue a diagnostic here + # error: [unknown-argument] "Argument `object` does not match any known parameter of overload 1 of class `type`" + # error: [missing-argument] "No argument provided for required parameter `o` of overload 1 of class `type`" + # error: [unknown-argument] "Argument `object` does not match any known parameter of overload 2 of class `type`" + # error: [missing-argument] "No arguments provided for required parameters `o`, `bases`, `dict` of overload 2 of class `type`" if type(object=x) is str: reveal_type(x) # revealed: str | int ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 695146c4bd77f..079315837a11e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2286,17 +2286,64 @@ impl<'db> Type<'db> { binding.into_outcome() } + Type::ClassLiteral(ClassLiteralType { class }) + if class.is_known(db, KnownClass::Type) => + { + // ```py + // class type: + // @overload + // def __init__(self, o: object, /) -> None: ... + // @overload + // def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... + // ``` + let overloads = Overloads::from_overloads([ + Signature::new( + Parameters::new([Parameter::new( + Some("o".into()), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + )]), + Some(self.to_instance(db)), + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some("o".into()), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("bases".into()), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("dict".into()), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + ), + ]), + Some(self.to_instance(db)), + ), + ]); + + let mut binding = bind_call(db, arguments, &overloads, self); + let Some((index, overload)) = binding.matching_overload_mut() else { + return Err(CallError::BindingError { binding }); + }; + if index == 0 { + if let Some(arg) = arguments.first_argument() { + overload.set_return_type(arg.to_meta_type(db)); + } + } + binding.into_outcome() + } + // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { class }) => { - let overload = OverloadBinding::from_return_type(match class.known(db) { - Some(KnownClass::Type) => arguments - .exactly_one_argument() - .map(|arg| arg.to_meta_type(db)) - .unwrap_or_else(|| KnownClass::Type.to_instance(db)), - - _ => Type::Instance(InstanceType { class }), - }); + let overload = + OverloadBinding::from_return_type(Type::Instance(InstanceType { class })); let binding = overload.into_binding(); binding.into_outcome() } diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index d5ca096ed9a9b..814db5324be27 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -35,14 +35,6 @@ impl<'a, 'db> CallArguments<'a, 'db> { self.0.first().map(Argument::ty) } - // TODO this should be eliminated in favor of [`bind_call`] - pub(crate) fn exactly_one_argument(&self) -> Option> { - match &*self.0 { - [arg] => Some(arg.ty()), - _ => None, - } - } - // TODO this should be eliminated in favor of [`bind_call`] pub(crate) fn second_argument(&self) -> Option> { self.0.get(1).map(Argument::ty) From 61df852ba9489d6213c7ab39431646a8008b9902 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 11:39:53 -0500 Subject: [PATCH 11/32] Use bind_call for all class instantiations --- crates/red_knot_python_semantic/src/types.rs | 7 +++---- .../src/types/signatures.rs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 079315837a11e..deb2e885fb480 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2341,10 +2341,9 @@ impl<'db> Type<'db> { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` - Type::ClassLiteral(ClassLiteralType { class }) => { - let overload = - OverloadBinding::from_return_type(Type::Instance(InstanceType { class })); - let binding = overload.into_binding(); + Type::ClassLiteral(ClassLiteralType { .. }) => { + let signature = Signature::new(Parameters::any(), Some(self.to_instance(db))); + let binding = bind_call(db, arguments, &signature.into(), self); binding.into_outcome() } diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index cc61cb237dd36..8582eb9d610ce 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -115,6 +115,22 @@ impl<'db> Parameters<'db> { Self(parameters.into_iter().collect()) } + /// Return dynamic parameters: (*args: Any, **kwargs: Any) + pub(crate) fn any() -> Self { + Self(vec![ + Parameter { + name: Some(Name::new_static("args")), + annotated_ty: Some(Type::any()), + kind: ParameterKind::Variadic, + }, + Parameter { + name: Some(Name::new_static("kwargs")), + annotated_ty: Some(Type::any()), + kind: ParameterKind::KeywordVariadic, + }, + ]) + } + /// Return todo parameters: (*args: Todo, **kwargs: Todo) fn todo() -> Self { Self(vec![ From 2a8f8a8de71390f6ac1130fc48e2a80d1f81c846 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 11:40:47 -0500 Subject: [PATCH 12/32] clippy --- crates/red_knot_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index deb2e885fb480..79c6a161a0215 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2397,7 +2397,7 @@ impl<'db> Type<'db> { Type::Intersection(_) => { let overloads = Overloads::todo("Type::Intersection.call()"); - let binding = bind_call(db, &arguments, &overloads, self); + let binding = bind_call(db, arguments, &overloads, self); binding.into_outcome() } From 48b2b568d009497ee8967383a4183f01ad7dc141 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 12:38:15 -0500 Subject: [PATCH 13/32] Add todo comment about short-circuiting binding --- crates/red_knot_python_semantic/src/types/call/bind.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 4a48d7d6a8c72..907648e4a8643 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -22,6 +22,8 @@ pub(crate) fn bind_call<'db>( overloads: &Overloads<'db>, callable_ty: Type<'db>, ) -> CallBinding<'db> { + // TODO: This checks every overload. Consider short-circuiting this loop once we find the first + // overload that is a successful match against the argument list. let overloads = overloads .iter() .map(|signature| bind_overload(db, arguments, signature)) From 0e28bd909483c02a92b495a2bebe51786b54593e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 13:51:34 -0500 Subject: [PATCH 14/32] Fix merge conflicts --- crates/red_knot_python_semantic/src/types.rs | 50 ++++++++++--------- .../src/types/signatures.rs | 16 ------ 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index c569d6487dd57..d6f9047bcb950 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2411,28 +2411,31 @@ impl<'db> Type<'db> { ]), None, ), - Signature::new(Parameters::new([ - Parameter::new( - Some("self".into()), - Some(KnownClass::FunctionType.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some("instance".into()), - Some(not_none), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some("owner".into()), - Some(UnionType::from_elements( - db, - [KnownClass::Type.to_instance(db), Type::none(db)], - )), - ParameterKind::PositionalOnly { - default_ty: Some(Type::none(db)), - }, - ), - ])), + Signature::new( + Parameters::new([ + Parameter::new( + Some("self".into()), + Some(KnownClass::FunctionType.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("instance".into()), + Some(not_none), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some("owner".into()), + Some(UnionType::from_elements( + db, + [KnownClass::Type.to_instance(db), Type::none(db)], + )), + ParameterKind::PositionalOnly { + default_ty: Some(Type::none(db)), + }, + ), + ]), + None, + ), ]); let mut binding = bind_call(db, arguments, &overloads, self); @@ -2784,7 +2787,8 @@ impl<'db> Type<'db> { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { .. }) => { - let signature = Signature::new(Parameters::any(), Some(self.to_instance(db))); + let signature = + Signature::new(Parameters::gradual_form(), Some(self.to_instance(db))); let binding = bind_call(db, arguments, &signature.into(), self); binding.into_outcome() } diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 8bd0de9074f2b..f826a1c863b73 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -141,22 +141,6 @@ impl<'db> Parameters<'db> { self.is_gradual } - /// Return dynamic parameters: (*args: Any, **kwargs: Any) - pub(crate) fn any() -> Self { - Self(vec![ - Parameter { - name: Some(Name::new_static("args")), - annotated_ty: Some(Type::any()), - kind: ParameterKind::Variadic, - }, - Parameter { - name: Some(Name::new_static("kwargs")), - annotated_ty: Some(Type::any()), - kind: ParameterKind::KeywordVariadic, - }, - ]) - } - /// Return todo parameters: (*args: Todo, **kwargs: Todo) pub(crate) fn todo() -> Self { Self { From 8f67fdae4b10f345ce4c99ca539be03e26b30cc5 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 13:55:25 -0500 Subject: [PATCH 15/32] Use bind_call for dynamic callables --- crates/red_knot_python_semantic/src/types.rs | 10 ++++++---- .../src/types/call.rs | 2 +- .../src/types/call/bind.rs | 19 +------------------ .../src/types/signatures.rs | 9 +++++++++ 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d6f9047bcb950..ad87b3ec62647 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -30,7 +30,7 @@ use crate::semantic_index::symbol::ScopeId; use crate::semantic_index::{imported_modules, semantic_index}; use crate::suppression::check_suppressions; use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; -use crate::types::call::{bind_call, CallArguments, CallOutcome, OverloadBinding, UnionCallError}; +use crate::types::call::{bind_call, CallArguments, CallOutcome, UnionCallError}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; use crate::types::infer::infer_unpack_types; @@ -2833,9 +2833,11 @@ impl<'db> Type<'db> { // Dynamic types are callable, and the return type is the same dynamic type. Similarly, // `Never` is always callable and returns `Never`. - Type::Dynamic(_) | Type::Never => OverloadBinding::from_return_type(self) - .into_binding() - .into_outcome(), + Type::Dynamic(_) | Type::Never => { + let overloads = Overloads::dynamic(self); + let binding = bind_call(db, arguments, &overloads, self); + binding.into_outcome() + } Type::Union(union) => { CallOutcome::try_call_union(db, union, |element| element.try_call(db, arguments)) diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index c5ac53901f0b8..ad66c28d1fdeb 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -6,7 +6,7 @@ use crate::Db; mod arguments; mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{bind_call, CallBinding, OverloadBinding}; +pub(super) use bind::{bind_call, CallBinding}; /// A successfully bound call where all arguments are valid. /// diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 907648e4a8643..dfee083f97686 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -7,7 +7,7 @@ use crate::types::diagnostic::{ TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; use crate::types::signatures::Parameter; -use crate::types::{todo_type, CallableType, UnionType}; +use crate::types::{CallableType, UnionType}; use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span}; use ruff_python_ast as ast; use ruff_text_size::Ranged; @@ -290,23 +290,6 @@ pub(crate) struct OverloadBinding<'db> { } impl<'db> OverloadBinding<'db> { - // TODO remove this constructor and construct always from `bind_call` - pub(crate) fn into_binding(self) -> CallBinding<'db> { - CallBinding { - callable_ty: todo_type!("CallBinding::from_return_type"), - overloads: vec![self].into_boxed_slice(), - } - } - - // TODO remove this constructor and construct always from `bind_call` - pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self { - Self { - return_ty, - parameter_tys: Box::default(), - errors: vec![], - } - } - pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { self.return_ty = return_ty; } diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index f826a1c863b73..8ae738d692575 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -33,6 +33,15 @@ impl<'db> Overloads<'db> { } } + /// Return a signature for a dynamic callable + pub(crate) fn dynamic(ty: Type<'db>) -> Self { + let signature = Signature { + parameters: Parameters::gradual_form(), + return_ty: Some(ty), + }; + signature.into() + } + /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo #[allow(unused_variables)] // 'reason' only unused in debug builds pub(crate) fn todo(reason: &'static str) -> Self { From 42188cd04f63a809dc9fe6bb02b4297cee7d07b4 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Mar 2025 14:02:28 -0500 Subject: [PATCH 16/32] Clean up this match --- crates/red_knot_python_semantic/src/types.rs | 83 ++++++++++---------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ad87b3ec62647..7e266ca814fe8 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2459,51 +2459,48 @@ impl<'db> Type<'db> { ))); } } else { - if let Some(instance) = arguments.second_argument() { - if instance.is_none(db) { + match (arguments.second_argument(), arguments.third_argument()) { + (Some(instance), _) if instance.is_none(db) => { overload.set_return_type(function_ty); - } else { - overload.set_return_type(match instance { - Type::KnownInstance(KnownInstanceType::TypeAliasType( - type_alias, - )) if arguments - .third_argument() - .and_then(Type::into_class_literal) - .is_some_and(|class_literal| { - class_literal - .class - .is_known(db, KnownClass::TypeAliasType) - }) - && function.name(db) == "__name__" => - { - Type::string_literal(db, type_alias.name(db)) - } - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) - if arguments - .third_argument() - .and_then(Type::into_class_literal) - .is_some_and(|class_literal| { - class_literal - .class - .is_known(db, KnownClass::TypeVar) - }) - && function.name(db) == "__name__" => - { - Type::string_literal(db, typevar.name(db)) - } - _ => { - if function - .has_known_class_decorator(db, KnownClass::Property) - { - todo_type!("@property") - } else { - Type::Callable(CallableType::BoundMethod( - BoundMethodType::new(db, function, instance), - )) - } - } - }); } + + ( + Some(Type::KnownInstance(KnownInstanceType::TypeAliasType( + type_alias, + ))), + Some(Type::ClassLiteral(ClassLiteralType { class })), + ) if class.is_known(db, KnownClass::TypeAliasType) + && function.name(db) == "__name__" => + { + overload + .set_return_type(Type::string_literal(db, type_alias.name(db))); + } + + ( + Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), + Some(Type::ClassLiteral(ClassLiteralType { class })), + ) if class.is_known(db, KnownClass::TypeVar) + && function.name(db) == "__name__" => + { + overload + .set_return_type(Type::string_literal(db, typevar.name(db))); + } + + (Some(_), _) + if function.has_known_class_decorator(db, KnownClass::Property) => + { + overload.set_return_type(todo_type!("@property")); + } + + (Some(instance), _) => { + overload.set_return_type(Type::Callable( + CallableType::BoundMethod(BoundMethodType::new( + db, function, instance, + )), + )); + } + + (None, _) => {} } } } From 045ff63de1fb986d17f532acd89042b867fec9d3 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 10:44:48 -0400 Subject: [PATCH 17/32] Fix merge conflicts --- crates/red_knot_python_semantic/src/types.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 66ccca17a6219..f0d108444a992 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2791,9 +2791,9 @@ impl<'db> Type<'db> { } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Dynamic(dynamic_type) => Ok(CallOutcome::Single( - CallBinding::from_return_type(Type::Dynamic(dynamic_type)), - )), + ClassBase::Dynamic(dynamic_type) => { + Type::Dynamic(dynamic_type).try_call(db, arguments) + } ClassBase::Class(class) => Type::class_literal(class).try_call(db, arguments), }, From 7ebda2b0e1f1a8bbb40069598f51fba288d813c7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 12:15:47 -0400 Subject: [PATCH 18/32] Use Name::new_static --- crates/red_knot_python_semantic/src/types.rs | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f0d108444a992..3a39f503af22f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2320,12 +2320,12 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::new( - Some("instance".into()), + Some(Name::new_static("instance")), Some(Type::none(db)), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("owner".into()), + Some(Name::new_static("owner")), Some(KnownClass::Type.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, ), @@ -2335,12 +2335,12 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::new( - Some("instance".into()), + Some(Name::new_static("instance")), Some(not_none), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("owner".into()), + Some(Name::new_static("owner")), Some(UnionType::from_elements( db, [KnownClass::Type.to_instance(db), Type::none(db)], @@ -2394,17 +2394,17 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::new( - Some("self".into()), + Some(Name::new_static("self")), Some(KnownClass::FunctionType.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("instance".into()), + Some(Name::new_static("instance")), Some(Type::none(db)), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("owner".into()), + Some(Name::new_static("owner")), Some(KnownClass::Type.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, ), @@ -2414,17 +2414,17 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::new( - Some("self".into()), + Some(Name::new_static("self")), Some(KnownClass::FunctionType.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("instance".into()), + Some(Name::new_static("instance")), Some(not_none), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("owner".into()), + Some(Name::new_static("owner")), Some(UnionType::from_elements( db, [KnownClass::Type.to_instance(db), Type::none(db)], @@ -2648,7 +2648,7 @@ impl<'db> Type<'db> { // ``` let signature = Signature::new( Parameters::new([Parameter::new( - Some("o".into()), + Some(Name::new_static("o")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: Some(Type::BooleanLiteral(false)), @@ -2683,7 +2683,7 @@ impl<'db> Type<'db> { let overloads = Overloads::from_overloads([ Signature::new( Parameters::new([Parameter::new( - Some("o".into()), + Some(Name::new_static("o")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: Some(Type::string_literal(db, "")), @@ -2694,17 +2694,17 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::new( - Some("o".into()), + Some(Name::new_static("o")), Some(Type::any()), // TODO: ReadableBuffer ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("encoding".into()), + Some(Name::new_static("encoding")), Some(KnownClass::Str.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("errors".into()), + Some(Name::new_static("errors")), Some(KnownClass::Str.to_instance(db)), ParameterKind::PositionalOnly { default_ty: None }, ), @@ -2741,7 +2741,7 @@ impl<'db> Type<'db> { let overloads = Overloads::from_overloads([ Signature::new( Parameters::new([Parameter::new( - Some("o".into()), + Some(Name::new_static("o")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: None }, )]), @@ -2750,17 +2750,17 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::new( - Some("o".into()), + Some(Name::new_static("o")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("bases".into()), + Some(Name::new_static("bases")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: None }, ), Parameter::new( - Some("dict".into()), + Some(Name::new_static("dict")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: None }, ), From eaa8832b6115a1be8a1a741f8a47b97aa9a04727 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 12:16:44 -0400 Subject: [PATCH 19/32] Document Overloads::from_overloads precondition --- crates/red_knot_python_semantic/src/types/signatures.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 8ae738d692575..a72b441aebfa9 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -11,6 +11,8 @@ pub enum Overloads<'db> { } impl<'db> Overloads<'db> { + /// Creates a new `Overloads` from an non-empty iterator of [`Signature`]s. Panics if the + /// iterator is empty. pub(crate) fn from_overloads(overloads: I) -> Self where I: IntoIterator, From d0d50299064b7934a845299b2e137bb7798d526a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 12:26:19 -0400 Subject: [PATCH 20/32] Add snapshot test for overload call binding errors --- .../diagnostics/invalid_argument_type.md | 11 ++++++ ...stics_-_Calls_to_overloaded_functions.snap | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 794c29b6b96fe..3ea55d4fee72f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -195,3 +195,14 @@ class C: c = C() c.square("hello") # error: [invalid-argument-type] ``` + +## Calls to overloaded functions + +TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in +real Python code. We are instead testing a special-cased function where we create an overloaded +signature internally. Update that this to an `@overload` function in the Python snippet itself once +we can. + +```py +type("Foo", ()) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap new file mode 100644 index 0000000000000..d461cd291bea0 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap @@ -0,0 +1,38 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to overloaded functions +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | type("Foo", ()) +``` + +# Diagnostics + +``` +error: lint:too-many-positional-arguments + --> /src/mdtest_snippet.py:1:13 + | +1 | type("Foo", ()) + | ^^ Too many positional arguments to overload 1 of class `type`: expected 1, got 2 + | + +``` + +``` +error: lint:missing-argument + --> /src/mdtest_snippet.py:1:1 + | +1 | type("Foo", ()) + | ^^^^^^^^^^^^^^^ No argument provided for required parameter `dict` of overload 2 of class `type` + | + +``` From cebd1eed910a240311a65a491878e57d0e2719d0 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 12:49:13 -0400 Subject: [PATCH 21/32] Single diagnostic for calls to overloaded function --- .../resources/mdtest/call/builtins.md | 7 +- .../resources/mdtest/call/methods.md | 17 ++--- .../resources/mdtest/descriptor_protocol.md | 21 ++---- .../diagnostics/invalid_argument_type.md | 2 +- .../resources/mdtest/narrow/type.md | 5 +- ...stics_-_Calls_to_overloaded_functions.snap | 18 +---- .../src/types/call/bind.rs | 73 +++++++------------ .../src/types/diagnostic.rs | 24 ++++++ 8 files changed, 71 insertions(+), 96 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md index 145d3bf7fb5bd..f8caef3a95042 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md @@ -32,12 +32,9 @@ reveal_type(type("Foo", (), {})) # revealed: type Other numbers of arguments are invalid ```py -# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of class `type`: expected 1, got 2" -# error: [missing-argument] "No argument provided for required parameter `dict` of overload 2 of class `type`" +# error: [no-matching-overload] "No overload of class `type` matches arguments" type("Foo", ()) -# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of class `type`: expected 1, got 3" -# error: [unknown-argument] "Argument `weird_other_arg` does not match any known parameter of overload 1 of class `type`" -# error: [unknown-argument] "Argument `weird_other_arg` does not match any known parameter of overload 2 of class `type`" +# error: [no-matching-overload] "No overload of class `type` matches arguments" type("Foo", (), {}, weird_other_arg=42) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 94abd144cc511..624fdbfc58714 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -235,30 +235,23 @@ method_wrapper(C(), None) method_wrapper(None, C) # Passing `None` without an `owner` argument is an -# error: [missing-argument] "No argument provided for required parameter `owner` of overload 1 of method wrapper `__get__` of function `f`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of overload 2 of method wrapper `__get__` of function `f`; expected type `~None`" +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" method_wrapper(None) # Passing something that is not assignable to `type` as the `owner` argument is an -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of overload 1 of method wrapper `__get__` of function `f`; expected type `type`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of overload 2 of method wrapper `__get__` of function `f`; expected type `~None`" -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of overload 2 of method wrapper `__get__` of function `f`; expected type `type | None`" +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" method_wrapper(None, 1) # Passing `None` as the `owner` argument when `instance` is `None` is an -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of overload 1 of method wrapper `__get__` of function `f`; expected type `type`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 1 (`instance`) of overload 2 of method wrapper `__get__` of function `f`; expected type `~None`" +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" method_wrapper(None, None) # Calling `__get__` without any arguments is an -# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of overload 1 of method wrapper `__get__` of function `f`" -# error: [missing-argument] "No argument provided for required parameter `instance` of overload 2 of method wrapper `__get__` of function `f`" +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" method_wrapper() # Calling `__get__` with too many positional arguments is an -# error: [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`instance`) of overload 1 of method wrapper `__get__` of function `f`; expected type `None`" -# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of method wrapper `__get__` of function `f`: expected 2, got 3" -# error: [too-many-positional-arguments] "Too many positional arguments to overload 2 of method wrapper `__get__` of function `f`: expected 2, got 3" +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" method_wrapper(C(), C, "one too many") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index c9e3e212fb956..a9e97b4467657 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -710,39 +710,30 @@ Finally, we test some error cases for the call to the wrapper descriptor: ```py # Calling the wrapper descriptor without any arguments is an -# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`, `owner` of overload 1 of wrapper descriptor `FunctionType.__get__`" -# error: [missing-argument] "No arguments provided for required parameters `self`, `instance` of overload 2 of wrapper descriptor `FunctionType.__get__`" +# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor() # Calling it without the `instance` argument is an also an -# error: [missing-argument] "No arguments provided for required parameters `instance`, `owner` of overload 1 of wrapper descriptor `FunctionType.__get__`" -# error: [missing-argument] "No argument provided for required parameter `instance` of overload 2 of wrapper descriptor `FunctionType.__get__`" +# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(f) # Calling it without the `owner` argument if `instance` is not `None` is an -# error: [missing-argument] "No argument provided for required parameter `owner` of overload 1 of wrapper descriptor `FunctionType.__get__`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(f, None) # But calling it with an instance is fine (in this case, the `owner` argument is optional): wrapper_descriptor(f, C()) # Calling it with something that is not a `FunctionType` as the first argument is an -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of overload 1 of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" -# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" +# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(1, None, type(f)) # Calling it with something that is not a `type` as the `owner` argument is an -# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of overload 1 of wrapper descriptor `FunctionType.__get__`; expected type `type`" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" -# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `type | None`" +# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(f, None, f) # Calling it with too many positional arguments is an -# error: [too-many-positional-arguments] "Too many positional arguments to overload 1 of wrapper descriptor `FunctionType.__get__`: expected 3, got 4" -# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`instance`) of overload 2 of wrapper descriptor `FunctionType.__get__`; expected type `~None`" -# error: [too-many-positional-arguments] "Too many positional arguments to overload 2 of wrapper descriptor `FunctionType.__get__`: expected 3, got 4" +# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(f, None, type(f), "one too many") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 3ea55d4fee72f..097965d4c016a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -204,5 +204,5 @@ signature internally. Update that this to an `@overload` function in the Python we can. ```py -type("Foo", ()) +type("Foo", ()) # error: [no-matching-overload] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index 579f5c574f8f8..1e3d5f2f1111d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -88,10 +88,7 @@ def _(x: str | int): ```py def _(x: str | int): - # error: [unknown-argument] "Argument `object` does not match any known parameter of overload 1 of class `type`" - # error: [missing-argument] "No argument provided for required parameter `o` of overload 1 of class `type`" - # error: [unknown-argument] "Argument `object` does not match any known parameter of overload 2 of class `type`" - # error: [missing-argument] "No arguments provided for required parameters `o`, `bases`, `dict` of overload 2 of class `type`" + # error: [no-matching-overload] "No overload of class `type` matches arguments" if type(object=x) is str: reveal_type(x) # revealed: str | int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap index d461cd291bea0..5d196b468614e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap @@ -12,27 +12,17 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ## mdtest_snippet.py ``` -1 | type("Foo", ()) +1 | type("Foo", ()) # error: [no-matching-overload] ``` # Diagnostics ``` -error: lint:too-many-positional-arguments - --> /src/mdtest_snippet.py:1:13 - | -1 | type("Foo", ()) - | ^^ Too many positional arguments to overload 1 of class `type`: expected 1, got 2 - | - -``` - -``` -error: lint:missing-argument +error: lint:no-matching-overload --> /src/mdtest_snippet.py:1:1 | -1 | type("Foo", ()) - | ^^^^^^^^^^^^^^^ No argument provided for required parameter `dict` of overload 2 of class `type` +1 | type("Foo", ()) # error: [no-matching-overload] + | ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments | ``` diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index dfee083f97686..4214c4f3b1a69 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -3,7 +3,7 @@ use super::{ }; use crate::db::Db; use crate::types::diagnostic::{ - INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED, + INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; use crate::types::signatures::Parameter; @@ -259,18 +259,33 @@ impl<'db> CallBinding<'db> { } /// Report diagnostics for all of the errors that occurred when trying to match actual - /// arguments to formal parameters. If the callable has multiple overloads, we report - /// diagnostics for all of them. + /// arguments to formal parameters. If the callable has multiple overloads, we report a single + /// diagnostic that we couldn't match any overload. + /// TODO: Update this to add subdiagnostics about how we failed to match each overload. pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { let callable_descriptor = self.callable_descriptor(context.db()); - let multiple_overloads = self.overloads.len() > 1; - for (idx, overload) in self.overloads.iter().enumerate() { + if self.overloads.len() > 1 { + context.report_lint( + &NO_MATCHING_OVERLOAD, + node, + format_args!( + "No overload{} matches arguments", + if let Some(CallableDescriptor { kind, name }) = callable_descriptor { + format!(" of {kind} `{name}`") + } else { + String::new() + } + ), + ); + return; + } + + for overload in &self.overloads { overload.report_diagnostics( context, node, self.callable_ty, callable_descriptor.as_ref(), - multiple_overloads.then(|| idx + 1), ); } } @@ -329,10 +344,9 @@ impl<'db> OverloadBinding<'db> { node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_descriptor: Option<&CallableDescriptor>, - overload: Option, ) { for error in &self.errors { - error.report_diagnostic(context, node, callable_ty, callable_descriptor, overload); + error.report_diagnostic(context, node, callable_ty, callable_descriptor); } } @@ -465,7 +479,6 @@ impl<'db> CallBindingError<'db> { node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_descriptor: Option<&CallableDescriptor>, - overload: Option, ) { match self { Self::InvalidArgumentType { @@ -491,12 +504,7 @@ impl<'db> CallBindingError<'db> { Self::get_node(node, *argument_index), format_args!( "Object of type `{provided_ty_display}` cannot be assigned to \ - parameter {parameter}{}{}; expected type `{expected_ty_display}`", - if let Some(overload) = overload { - format!(" of overload {overload}") - } else { - String::new() - }, + parameter {parameter}{}; expected type `{expected_ty_display}`", if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { @@ -516,20 +524,10 @@ impl<'db> CallBindingError<'db> { &TOO_MANY_POSITIONAL_ARGUMENTS, Self::get_node(node, *first_excess_argument_index), format_args!( - "Too many positional arguments{}{}{}: expected \ + "Too many positional arguments{}: expected \ {expected_positional_count}, got {provided_positional_count}", - if overload.is_some() || callable_descriptor.is_some() { - " to" - } else { - "" - }, - if let Some(overload) = overload { - format!(" overload {overload} of") - } else { - String::new() - }, if let Some(CallableDescriptor { kind, name }) = callable_descriptor { - format!(" {kind} `{name}`") + format!(" to {kind} `{name}`") } else { String::new() } @@ -543,12 +541,7 @@ impl<'db> CallBindingError<'db> { &MISSING_ARGUMENT, node, format_args!( - "No argument{s} provided for required parameter{s} {parameters}{}{}", - if let Some(overload) = overload { - format!(" of overload {overload}") - } else { - String::new() - }, + "No argument{s} provided for required parameter{s} {parameters}{}", if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { @@ -566,12 +559,7 @@ impl<'db> CallBindingError<'db> { &UNKNOWN_ARGUMENT, Self::get_node(node, *argument_index), format_args!( - "Argument `{argument_name}` does not match any known parameter{}{}", - if let Some(overload) = overload { - format!(" of overload {overload}") - } else { - String::new() - }, + "Argument `{argument_name}` does not match any known parameter{}", if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { @@ -589,12 +577,7 @@ impl<'db> CallBindingError<'db> { &PARAMETER_ALREADY_ASSIGNED, Self::get_node(node, *argument_index), format_args!( - "Multiple values provided for parameter {parameter}{}{}", - if let Some(overload) = overload { - format!(" of overload {overload}") - } else { - String::new() - }, + "Multiple values provided for parameter {parameter}{}", if let Some(CallableDescriptor { kind, name }) = callable_descriptor { format!(" of {kind} `{name}`") } else { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 8646942c304cd..8a88541d2ad27 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -45,6 +45,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_TYPE_FORM); registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); registry.register_lint(&MISSING_ARGUMENT); + registry.register_lint(&NO_MATCHING_OVERLOAD); registry.register_lint(&NON_SUBSCRIPTABLE); registry.register_lint(&NOT_ITERABLE); registry.register_lint(&UNSUPPORTED_BOOL_CONVERSION); @@ -474,6 +475,29 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for calls to an overloaded function that do not match any of the overloads. + /// + /// ## Why is this bad? + /// Failing to provide the correct arguments to one of the overloads will raise a `TypeError` + /// at runtime. + /// + /// ## Examples + /// ```python + /// @overload + /// def func(x: int): ... + /// @overload + /// def func(x: bool): ... + /// func("string") # error: [no-matching-overload] + /// ``` + pub(crate) static NO_MATCHING_OVERLOAD = { + summary: "detects calls that do not match any overload", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for subscripting objects that do not support subscripting. From 1447cb3c0703ceb428fbaf3fab8bd644be5b315b Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 12:54:24 -0400 Subject: [PATCH 22/32] Update lint schema --- knot.schema.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/knot.schema.json b/knot.schema.json index 5c757109b1d33..c0d82b5c68757 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -491,6 +491,16 @@ } ] }, + "no-matching-overload": { + "title": "detects calls that do not match any overload", + "description": "## What it does\nChecks for calls to an overloaded function that do not match any of the overloads.\n\n## Why is this bad?\nFailing to provide the correct arguments to one of the overloads will raise a `TypeError`\nat runtime.\n\n## Examples\n```python\n@overload\ndef func(x: int): ...\n@overload\ndef func(x: bool): ...\nfunc(\"string\") # error: [no-matching-overload]\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "non-subscriptable": { "title": "detects subscripting objects that do not support subscripting", "description": "## What it does\nChecks for subscripting objects that do not support subscripting.\n\n## Why is this bad?\nSubscripting an object that does not support it will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4[1] # TypeError: 'int' object is not subscriptable\n```", From b63286e583007c905e4c63ff72f79c9d7ec26a27 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 10 Mar 2025 12:58:56 -0400 Subject: [PATCH 23/32] typo --- .../resources/mdtest/diagnostics/invalid_argument_type.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 097965d4c016a..edac2a309c4c0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -200,8 +200,8 @@ c.square("hello") # error: [invalid-argument-type] TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in real Python code. We are instead testing a special-cased function where we create an overloaded -signature internally. Update that this to an `@overload` function in the Python snippet itself once -we can. +signature internally. Update this to an `@overload` function in the Python snippet itself once we +can. ```py type("Foo", ()) # error: [no-matching-overload] From ea088e4f6b3c6d3669972d514f13cbed660b380e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 10:29:51 -0400 Subject: [PATCH 24/32] Move overload diagnostic check into separate file --- .../mdtest/diagnostics/invalid_argument_type.md | 11 ----------- .../mdtest/diagnostics/no_matching_overload.md | 14 ++++++++++++++ ...agnostics_-_Calls_to_overloaded_functions.snap} | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap => no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap} (76%) diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index edac2a309c4c0..794c29b6b96fe 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -195,14 +195,3 @@ class C: c = C() c.square("hello") # error: [invalid-argument-type] ``` - -## Calls to overloaded functions - -TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in -real Python code. We are instead testing a special-cased function where we create an overloaded -signature internally. Update this to an `@overload` function in the Python snippet itself once we -can. - -```py -type("Foo", ()) # error: [no-matching-overload] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md new file mode 100644 index 0000000000000..d0d3c916df9d2 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md @@ -0,0 +1,14 @@ +# No matching overload diagnostics + + + +## Calls to overloaded functions + +TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in +real Python code. We are instead testing a special-cased function where we create an overloaded +signature internally. Update this to an `@overload` function in the Python snippet itself once we +can. + +```py +type("Foo", ()) # error: [no-matching-overload] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap similarity index 76% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap index 5d196b468614e..c824ee37da98f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_overloaded_functions.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap @@ -3,8 +3,8 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to overloaded functions -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md --- # Python source files From 5cd1eba2c2e8cbbd1c9a258a9a6a6eb67d662fec Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 10:42:31 -0400 Subject: [PATCH 25/32] Add TODOs about real overload algorithm --- .../red_knot_python_semantic/src/types/call/bind.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 4214c4f3b1a69..0dd8a4d096f45 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -22,8 +22,12 @@ pub(crate) fn bind_call<'db>( overloads: &Overloads<'db>, callable_ty: Type<'db>, ) -> CallBinding<'db> { - // TODO: This checks every overload. Consider short-circuiting this loop once we find the first - // overload that is a successful match against the argument list. + // TODO: This checks every overload. In the proposed more detailed call checking spec [1], + // arguments are checked for arity first, and are only checked for type assignability against + // the matching overloads. Make sure to implement that as part of separating call binding into + // two phases. + // + // [1] https://github.com/python/typing/pull/1839 let overloads = overloads .iter() .map(|signature| bind_overload(db, arguments, signature)) @@ -169,9 +173,14 @@ pub(crate) struct CallableDescriptor<'a> { /// If the callable has multiple overloads, the first one that matches is used as the overall /// binding match. /// +/// TODO: Implement the call site evaluation algorithm in the [proposed updated typing +/// spec][overloads], which is much more subtle than “first match wins”. +/// /// If the arguments cannot be matched to formal parameters, we store information about the /// specific errors that occurred when trying to match them up. If the callable has multiple /// overloads, we store this error information for each overload. +/// +/// [overloads]: https://github.com/python/typing/pull/1839 #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct CallBinding<'db> { /// Type of the callable object (function, class...) From a75c724a5a72c5017482cad46ad8cfc4e9999c51 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 10:43:17 -0400 Subject: [PATCH 26/32] Simpler `has_binding_errors` --- crates/red_knot_python_semantic/src/types/call/bind.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 0dd8a4d096f45..a952cbf564a7f 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -204,9 +204,7 @@ impl<'db> CallBinding<'db> { /// Returns whether there were any errors binding this call site. If the callable has multiple /// overloads, they must _all_ have errors. pub(crate) fn has_binding_errors(&self) -> bool { - self.overloads - .iter() - .all(OverloadBinding::has_binding_errors) + self.matching_overload().is_none() } /// Returns the overload that matched for this call binding. Returns `None` if none of the From 0315a0458894b521ddc6beaa71fadc2d0078082a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 10:50:02 -0400 Subject: [PATCH 27/32] Return Type::Unknown for invalid overload call return type --- crates/red_knot_python_semantic/src/types.rs | 2 +- crates/red_knot_python_semantic/src/types/call.rs | 8 ++++---- .../red_knot_python_semantic/src/types/call/bind.rs | 11 +++++++---- crates/red_knot_python_semantic/src/types/class.rs | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3a39f503af22f..94994abd9b98f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2161,7 +2161,7 @@ impl<'db> Type<'db> { match err { CallError::BindingError { binding } => { return Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(binding.return_type(db)), + truthiness: type_to_truthiness(binding.return_type()), not_boolable_type: *instance_ty, }); } diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index ad66c28d1fdeb..fe82ba38b8661 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -68,9 +68,9 @@ impl<'db> CallOutcome<'db> { /// The type returned by this call. pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { match self { - Self::Single(binding) => binding.return_type(db), + Self::Single(binding) => binding.return_type(), Self::Union(bindings) => { - UnionType::from_elements(db, bindings.iter().map(|binding| binding.return_type(db))) + UnionType::from_elements(db, bindings.iter().map(CallBinding::return_type)) } } } @@ -123,11 +123,11 @@ impl<'db> CallError<'db> { db, bindings .iter() - .map(|binding| binding.return_type(db)) + .map(CallBinding::return_type) .chain(errors.iter().map(|err| err.fallback_return_type(db))), )), Self::PossiblyUnboundDunderCall { outcome, .. } => Some(outcome.return_type(db)), - Self::BindingError { binding } => Some(binding.return_type(db)), + Self::BindingError { binding } => Some(binding.return_type()), } } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index a952cbf564a7f..3e721efe8adae 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -225,16 +225,19 @@ impl<'db> CallBinding<'db> { .find(|(_, overload)| !overload.has_binding_errors()) } - /// Returns the return type of the matching overload for this binding. If none of the overloads - /// matched, returns a union of the return types of each overload. - pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + /// Returns the return type of this call. For a valid call, this is the return type of the + /// overload that the arguments matched against. For an invalid call to a non-overloaded + /// function, this is the return type of the function. For an invalid call to an overloaded + /// function, we return `Type::unknown`, since we cannot make any useful conclusions about + /// which overload was intended to be called. + pub(crate) fn return_type(&self) -> Type<'db> { if let Some((_, overload)) = self.matching_overload() { return overload.return_type(); } if let [overload] = self.overloads.as_ref() { return overload.return_type(); } - UnionType::from_elements(db, self.overloads.iter().map(OverloadBinding::return_type)) + Type::unknown() } fn callable_descriptor(&self, db: &'db dyn Db) -> Option { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index ed9a335266faf..04854b7c2e7a5 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -251,7 +251,7 @@ impl<'db> Class<'db> { }) .map(|mut builder| { for binding in bindings { - builder = builder.add(binding.return_type(db)); + builder = builder.add(binding.return_type()); } builder.build() @@ -272,7 +272,7 @@ impl<'db> Class<'db> { // TODO we should also check for binding errors that would indicate the metaclass // does not accept the right arguments - Err(CallError::BindingError { binding }) => Ok(binding.return_type(db)), + Err(CallError::BindingError { binding }) => Ok(binding.return_type()), }; return return_ty_result.map(|ty| ty.to_meta_type(db)); From d7fa0d18c019ddb3e96e0a818932c5fa7abcd623 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 11:30:18 -0400 Subject: [PATCH 28/32] Remove extra parameter accessors --- crates/red_knot_python_semantic/src/types.rs | 81 +++++++++---------- .../src/types/call/bind.rs | 21 ----- .../src/types/infer.rs | 6 +- 3 files changed, 43 insertions(+), 65 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 94994abd9b98f..1d17ab2a4a033 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2515,56 +2515,58 @@ impl<'db> Type<'db> { match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { - let (ty_a, ty_b) = overload - .two_parameter_types() - .unwrap_or((Type::unknown(), Type::unknown())); - overload - .set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b))); + if let [ty_a, ty_b] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_equivalent_to(db, *ty_b), + )); + } } Some(KnownFunction::IsSubtypeOf) => { - let (ty_a, ty_b) = overload - .two_parameter_types() - .unwrap_or((Type::unknown(), Type::unknown())); - overload - .set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b))); + if let [ty_a, ty_b] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_subtype_of(db, *ty_b), + )); + } } Some(KnownFunction::IsAssignableTo) => { - let (ty_a, ty_b) = overload - .two_parameter_types() - .unwrap_or((Type::unknown(), Type::unknown())); - overload - .set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b))); + if let [ty_a, ty_b] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_assignable_to(db, *ty_b), + )); + } } Some(KnownFunction::IsDisjointFrom) => { - let (ty_a, ty_b) = overload - .two_parameter_types() - .unwrap_or((Type::unknown(), Type::unknown())); - overload - .set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b))); + if let [ty_a, ty_b] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_disjoint_from(db, *ty_b), + )); + } } Some(KnownFunction::IsGradualEquivalentTo) => { - let (ty_a, ty_b) = overload - .two_parameter_types() - .unwrap_or((Type::unknown(), Type::unknown())); - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_gradual_equivalent_to(db, ty_b), - )); + if let [ty_a, ty_b] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_gradual_equivalent_to(db, *ty_b), + )); + } } Some(KnownFunction::IsFullyStatic) => { - let ty = overload.one_parameter_type().unwrap_or(Type::unknown()); - overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); + if let [ty] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); + } } Some(KnownFunction::IsSingleton) => { - let ty = overload.one_parameter_type().unwrap_or(Type::unknown()); - overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); + if let [ty] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); + } } Some(KnownFunction::IsSingleValued) => { - let ty = overload.one_parameter_type().unwrap_or(Type::unknown()); - overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); + if let [ty] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); + } } Some(KnownFunction::Len) => { - if let Some(first_arg) = overload.one_parameter_type() { + if let [first_arg] = overload.parameter_types() { if let Some(len_ty) = first_arg.len(db) { overload.set_return_type(len_ty); } @@ -2572,16 +2574,15 @@ impl<'db> Type<'db> { } Some(KnownFunction::Repr) => { - if let Some(first_arg) = overload.one_parameter_type() { + if let [first_arg] = overload.parameter_types() { overload.set_return_type(first_arg.repr(db)); }; } Some(KnownFunction::Cast) => { - // TODO: Use `.two_parameter_tys()` exclusively - // when overloads are supported. + // TODO: Use `.parameter_types()` exclusively when overloads are supported. if let Some(casted_ty) = arguments.first_argument() { - if overload.two_parameter_types().is_some() { + if let [_, _] = overload.parameter_types() { overload.set_return_type(casted_ty); } }; @@ -2592,9 +2593,7 @@ impl<'db> Type<'db> { } Some(KnownFunction::GetattrStatic) => { - let Some((instance_ty, attr_name, default)) = - overload.three_parameter_types() - else { + let [instance_ty, attr_name, default] = overload.parameter_types() else { return binding.into_outcome(); }; @@ -2605,7 +2604,7 @@ impl<'db> Type<'db> { let default = if default.is_unknown() { Type::Never } else { - default + *default }; let union_with_default = |ty| UnionType::from_elements(db, [ty, default]); diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 3e721efe8adae..03aaf0f1902e0 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -327,27 +327,6 @@ impl<'db> OverloadBinding<'db> { &self.parameter_tys } - pub(crate) fn one_parameter_type(&self) -> Option> { - match self.parameter_types() { - [ty] => Some(*ty), - _ => None, - } - } - - pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> { - match self.parameter_types() { - [first, second] => Some((*first, *second)), - _ => None, - } - } - - pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> { - match self.parameter_types() { - [first, second, third] => Some((*first, *second, *third)), - _ => None, - } - } - fn report_diagnostics( &self, context: &InferContext<'db>, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 0b4ff1f6e0235..acc39e38c35b2 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3332,7 +3332,7 @@ impl<'db> TypeInferenceBuilder<'db> { match known_function { KnownFunction::RevealType => { - if let Some(revealed_type) = overload.one_parameter_type() { + if let [revealed_type] = overload.parameter_types() { self.context.report_diagnostic( call_expression, DiagnosticId::RevealedType, @@ -3361,7 +3361,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } KnownFunction::StaticAssert => { - if let Some((parameter_ty, message)) = overload.two_parameter_types() { + if let [parameter_ty, message] = overload.parameter_types() { let truthiness = match parameter_ty.try_bool(self.db()) { Ok(truthiness) => truthiness, Err(err) => { @@ -3392,7 +3392,7 @@ impl<'db> TypeInferenceBuilder<'db> { call_expression, format_args!("Static assertion error: {message}"), ); - } else if parameter_ty == Type::BooleanLiteral(false) { + } else if *parameter_ty == Type::BooleanLiteral(false) { self.context.report_lint( &STATIC_ASSERT_ERROR, call_expression, From 0b3caa2b25d584b6c2aa9a561c6973f56fe7db02 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 11:43:21 -0400 Subject: [PATCH 29/32] Use nullary salsa queries as LazyLock alternative --- crates/red_knot_python_semantic/src/types.rs | 334 ++++++++++--------- 1 file changed, 175 insertions(+), 159 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1d17ab2a4a033..f433ac336b3c7 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2315,46 +2315,49 @@ impl<'db> Type<'db> { // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... // ``` - let not_none = Type::none(db).negate(db); - let overloads = Overloads::from_overloads([ - Signature::new( - Parameters::new([ - Parameter::new( - Some(Name::new_static("instance")), - Some(Type::none(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("owner")), - Some(KnownClass::Type.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - ]), - None, - ), - Signature::new( - Parameters::new([ - Parameter::new( - Some(Name::new_static("instance")), - Some(not_none), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("owner")), - Some(UnionType::from_elements( - db, - [KnownClass::Type.to_instance(db), Type::none(db)], - )), - ParameterKind::PositionalOnly { - default_ty: Some(Type::none(db)), - }, - ), - ]), - None, - ), - ]); + #[salsa::tracked(return_ref)] + fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + let not_none = Type::none(db).negate(db); + Overloads::from_overloads([ + Signature::new( + Parameters::new([ + Parameter::new( + Some(Name::new_static("instance")), + Some(Type::none(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("owner")), + Some(KnownClass::Type.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + ]), + None, + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some(Name::new_static("instance")), + Some(not_none), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("owner")), + Some(UnionType::from_elements( + db, + [KnownClass::Type.to_instance(db), Type::none(db)], + )), + ParameterKind::PositionalOnly { + default_ty: Some(Type::none(db)), + }, + ), + ]), + None, + ), + ]) + } - let mut binding = bind_call(db, arguments, &overloads, self); + let mut binding = bind_call(db, arguments, overloads(db), self); let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2389,56 +2392,59 @@ impl<'db> Type<'db> { // Here, we also model `types.FunctionType.__get__`, but now we consider a call to // this as a function, i.e. we also expect the `self` argument to be passed in. - let not_none = Type::none(db).negate(db); - let overloads = Overloads::from_overloads([ - Signature::new( - Parameters::new([ - Parameter::new( - Some(Name::new_static("self")), - Some(KnownClass::FunctionType.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("instance")), - Some(Type::none(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("owner")), - Some(KnownClass::Type.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - ]), - None, - ), - Signature::new( - Parameters::new([ - Parameter::new( - Some(Name::new_static("self")), - Some(KnownClass::FunctionType.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("instance")), - Some(not_none), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("owner")), - Some(UnionType::from_elements( - db, - [KnownClass::Type.to_instance(db), Type::none(db)], - )), - ParameterKind::PositionalOnly { - default_ty: Some(Type::none(db)), - }, - ), - ]), - None, - ), - ]); + #[salsa::tracked(return_ref)] + fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + let not_none = Type::none(db).negate(db); + Overloads::from_overloads([ + Signature::new( + Parameters::new([ + Parameter::new( + Some(Name::new_static("self")), + Some(KnownClass::FunctionType.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("instance")), + Some(Type::none(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("owner")), + Some(KnownClass::Type.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + ]), + None, + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some(Name::new_static("self")), + Some(KnownClass::FunctionType.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("instance")), + Some(not_none), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("owner")), + Some(UnionType::from_elements( + db, + [KnownClass::Type.to_instance(db), Type::none(db)], + )), + ParameterKind::PositionalOnly { + default_ty: Some(Type::none(db)), + }, + ), + ]), + None, + ), + ]) + } - let mut binding = bind_call(db, arguments, &overloads, self); + let mut binding = bind_call(db, arguments, overloads(db), self); let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2645,18 +2651,22 @@ impl<'db> Type<'db> { // class bool(int): // def __new__(cls, o: object = ..., /) -> Self: ... // ``` - let signature = Signature::new( - Parameters::new([Parameter::new( - Some(Name::new_static("o")), - Some(Type::any()), - ParameterKind::PositionalOnly { - default_ty: Some(Type::BooleanLiteral(false)), - }, - )]), - Some(self.to_instance(db)), - ); + #[salsa::tracked(return_ref)] + fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + Signature::new( + Parameters::new([Parameter::new( + Some(Name::new_static("o")), + Some(Type::any()), + ParameterKind::PositionalOnly { + default_ty: Some(Type::BooleanLiteral(false)), + }, + )]), + Some(KnownClass::Bool.to_instance(db)), + ) + .into() + } - let mut binding = bind_call(db, arguments, &signature.into(), self); + let mut binding = bind_call(db, arguments, overloads(db), self); let Some((_, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2679,40 +2689,43 @@ impl<'db> Type<'db> { // @overload // def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ... // ``` - let overloads = Overloads::from_overloads([ - Signature::new( - Parameters::new([Parameter::new( - Some(Name::new_static("o")), - Some(Type::any()), - ParameterKind::PositionalOnly { - default_ty: Some(Type::string_literal(db, "")), - }, - )]), - Some(self.to_instance(db)), - ), - Signature::new( - Parameters::new([ - Parameter::new( + #[salsa::tracked(return_ref)] + fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + Overloads::from_overloads([ + Signature::new( + Parameters::new([Parameter::new( Some(Name::new_static("o")), - Some(Type::any()), // TODO: ReadableBuffer - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("encoding")), - Some(KnownClass::Str.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("errors")), - Some(KnownClass::Str.to_instance(db)), - ParameterKind::PositionalOnly { default_ty: None }, - ), - ]), - Some(self.to_instance(db)), - ), - ]); + Some(Type::any()), + ParameterKind::PositionalOnly { + default_ty: Some(Type::string_literal(db, "")), + }, + )]), + Some(KnownClass::Str.to_instance(db)), + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some(Name::new_static("o")), + Some(Type::any()), // TODO: ReadableBuffer + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("encoding")), + Some(KnownClass::Str.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("errors")), + Some(KnownClass::Str.to_instance(db)), + ParameterKind::PositionalOnly { default_ty: None }, + ), + ]), + Some(KnownClass::Str.to_instance(db)), + ), + ]) + } - let mut binding = bind_call(db, arguments, &overloads, self); + let mut binding = bind_call(db, arguments, overloads(db), self); let Some((index, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; @@ -2737,38 +2750,41 @@ impl<'db> Type<'db> { // @overload // def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... // ``` - let overloads = Overloads::from_overloads([ - Signature::new( - Parameters::new([Parameter::new( - Some(Name::new_static("o")), - Some(Type::any()), - ParameterKind::PositionalOnly { default_ty: None }, - )]), - Some(self.to_instance(db)), - ), - Signature::new( - Parameters::new([ - Parameter::new( + #[salsa::tracked(return_ref)] + fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + Overloads::from_overloads([ + Signature::new( + Parameters::new([Parameter::new( Some(Name::new_static("o")), Some(Type::any()), ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("bases")), - Some(Type::any()), - ParameterKind::PositionalOnly { default_ty: None }, - ), - Parameter::new( - Some(Name::new_static("dict")), - Some(Type::any()), - ParameterKind::PositionalOnly { default_ty: None }, - ), - ]), - Some(self.to_instance(db)), - ), - ]); + )]), + Some(KnownClass::Type.to_instance(db)), + ), + Signature::new( + Parameters::new([ + Parameter::new( + Some(Name::new_static("o")), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("bases")), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + ), + Parameter::new( + Some(Name::new_static("dict")), + Some(Type::any()), + ParameterKind::PositionalOnly { default_ty: None }, + ), + ]), + Some(KnownClass::Type.to_instance(db)), + ), + ]) + } - let mut binding = bind_call(db, arguments, &overloads, self); + let mut binding = bind_call(db, arguments, overloads(db), self); let Some((index, overload)) = binding.matching_overload_mut() else { return Err(CallError::BindingError { binding }); }; From 5937f240dd8a6ef133c68f9b447ca5166c5be354 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 11:46:09 -0400 Subject: [PATCH 30/32] Add TODO about merging decorator cases --- crates/red_knot_python_semantic/src/types.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f433ac336b3c7..66cc373db655f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2392,6 +2392,9 @@ impl<'db> Type<'db> { // Here, we also model `types.FunctionType.__get__`, but now we consider a call to // this as a function, i.e. we also expect the `self` argument to be passed in. + // TODO: Consider merging this signature with the one in the previous match clause, + // since the previous one is just this signature with the `self` parameters + // removed. #[salsa::tracked(return_ref)] fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { let not_none = Type::none(db).negate(db); From fdc67f973547b1f1f30bfce4e62cc9d83b3c1e10 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 13:33:11 -0400 Subject: [PATCH 31/32] Rename `Overloads` and `Signature` --- crates/red_knot_python_semantic/src/types.rs | 28 ++++++------- .../src/types/call.rs | 2 +- .../src/types/call/bind.rs | 7 ++-- .../src/types/infer.rs | 4 +- .../src/types/signatures.rs | 40 ++++++++++++------- 5 files changed, 47 insertions(+), 34 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 66cc373db655f..e68a28b1008d3 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -20,7 +20,7 @@ pub(crate) use self::infer::{ infer_scope_types, }; pub use self::narrow::KnownConstraintFunction; -pub(crate) use self::signatures::{Overloads, Signature}; +pub(crate) use self::signatures::{CallableSignature, Signature}; pub use self::subclass_of::SubclassOfType; use crate::module_name::ModuleName; use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; @@ -2316,9 +2316,9 @@ impl<'db> Type<'db> { // ``` #[salsa::tracked(return_ref)] - fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> { let not_none = Type::none(db).negate(db); - Overloads::from_overloads([ + CallableSignature::from_overloads([ Signature::new( Parameters::new([ Parameter::new( @@ -2396,9 +2396,9 @@ impl<'db> Type<'db> { // since the previous one is just this signature with the `self` parameters // removed. #[salsa::tracked(return_ref)] - fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> { let not_none = Type::none(db).negate(db); - Overloads::from_overloads([ + CallableSignature::from_overloads([ Signature::new( Parameters::new([ Parameter::new( @@ -2655,7 +2655,7 @@ impl<'db> Type<'db> { // def __new__(cls, o: object = ..., /) -> Self: ... // ``` #[salsa::tracked(return_ref)] - fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { + fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> { Signature::new( Parameters::new([Parameter::new( Some(Name::new_static("o")), @@ -2693,8 +2693,8 @@ impl<'db> Type<'db> { // def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ... // ``` #[salsa::tracked(return_ref)] - fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { - Overloads::from_overloads([ + fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> { + CallableSignature::from_overloads([ Signature::new( Parameters::new([Parameter::new( Some(Name::new_static("o")), @@ -2754,8 +2754,8 @@ impl<'db> Type<'db> { // def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... // ``` #[salsa::tracked(return_ref)] - fn overloads<'db>(db: &'db dyn Db) -> Overloads<'db> { - Overloads::from_overloads([ + fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> { + CallableSignature::from_overloads([ Signature::new( Parameters::new([Parameter::new( Some(Name::new_static("o")), @@ -2856,7 +2856,7 @@ impl<'db> Type<'db> { // Dynamic types are callable, and the return type is the same dynamic type. Similarly, // `Never` is always callable and returns `Never`. Type::Dynamic(_) | Type::Never => { - let overloads = Overloads::dynamic(self); + let overloads = CallableSignature::dynamic(self); let binding = bind_call(db, arguments, &overloads, self); binding.into_outcome() } @@ -2866,7 +2866,7 @@ impl<'db> Type<'db> { } Type::Intersection(_) => { - let overloads = Overloads::todo("Type::Intersection.call()"); + let overloads = CallableSignature::todo("Type::Intersection.call()"); let binding = bind_call(db, arguments, &overloads, self); binding.into_outcome() } @@ -4310,7 +4310,7 @@ impl<'db> FunctionType<'db> { /// Were this not a salsa query, then the calling query /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(return_ref)] - pub fn signature(self, db: &'db dyn Db) -> Overloads<'db> { + pub fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { let internal_signature = self.internal_signature(db).into(); let decorators = self.decorators(db); @@ -4323,7 +4323,7 @@ impl<'db> FunctionType<'db> { { internal_signature } else { - Overloads::todo("return type of decorated function") + CallableSignature::todo("return type of decorated function") } } else { internal_signature diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index fe82ba38b8661..475326f95792c 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -1,5 +1,5 @@ use super::context::InferContext; -use super::{Overloads, Signature, Type}; +use super::{CallableSignature, Signature, Type}; use crate::types::UnionType; use crate::Db; diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 03aaf0f1902e0..20ff424ac354e 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -1,5 +1,6 @@ use super::{ - Argument, CallArguments, CallError, CallOutcome, InferContext, Overloads, Signature, Type, + Argument, CallArguments, CallError, CallOutcome, CallableSignature, InferContext, Signature, + Type, }; use crate::db::Db; use crate::types::diagnostic::{ @@ -12,14 +13,14 @@ use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span}; use ruff_python_ast as ast; use ruff_text_size::Ranged; -/// Bind a [`CallArguments`] against a callable [`Signature`]. +/// Bind a [`CallArguments`] against a [`CallableSignature`]. /// /// The returned [`CallBinding`] provides the return type of the call, the bound types for all /// parameters, and any errors resulting from binding the call. pub(crate) fn bind_call<'db>( db: &'db dyn Db, arguments: &CallArguments<'_, 'db>, - overloads: &Overloads<'db>, + overloads: &CallableSignature<'db>, callable_ty: Type<'db>, ) -> CallBinding<'db> { // TODO: This checks every overload. In the proposed more detailed call checking spec [1], diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index acc39e38c35b2..d016a2ba7233e 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -6057,8 +6057,8 @@ impl<'db> TypeInferenceBuilder<'db> { Signature::new(parameters, Some(return_type)), ))); - // `Signature` / `Parameters` are not a `Type` variant, so we're storing the outer - // callable type on the these expressions instead. + // `Signature` / `Parameters` are not a `Type` variant, so we're storing + // the outer callable type on the these expressions instead. self.store_expression_type(arguments_slice, callable_type); self.store_expression_type(first_argument, callable_type); diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index a72b441aebfa9..d700fc0ef1f60 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -1,18 +1,31 @@ +//! _Signatures_ describe the expected parameters and return type of a function or other callable. +//! Overloads and unions add complexity to this simple description. +//! +//! In a call expression, the type of the callable might be a union of several types. The call must +//! be compatible with _all_ of these types, since at runtime the callable might be an instance of +//! any of them. +//! +//! Each of the atomic types in the union must be callable. Each callable might be _overloaded_, +//! containing multiple _overload signatures_, each of which describes a different combination of +//! argument types and return types. For each callable type in the union, the call expression's +//! arguments must match _at least one_ overload. + use super::{definition_expression_type, DynamicType, Type}; use crate::Db; use crate::{semantic_index::definition::Definition, types::todo_type}; use ruff_python_ast::{self as ast, name::Name}; -/// A collection of overloads for a callable. +/// The signature of a single callable. If the callable is overloaded, there is a separate +/// [`Signature`] for each overload. #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub enum Overloads<'db> { +pub enum CallableSignature<'db> { Single(Signature<'db>), Overloaded(Box<[Signature<'db>]>), } -impl<'db> Overloads<'db> { - /// Creates a new `Overloads` from an non-empty iterator of [`Signature`]s. Panics if the - /// iterator is empty. +impl<'db> CallableSignature<'db> { + /// Creates a new `CallableSignature` from an non-empty iterator of [`Signature`]s. + /// Panics if the iterator is empty. pub(crate) fn from_overloads(overloads: I) -> Self where I: IntoIterator, @@ -21,17 +34,17 @@ impl<'db> Overloads<'db> { let mut iter = overloads.into_iter(); let first_overload = iter.next().expect("overloads should not be empty"); let Some(second_overload) = iter.next() else { - return Overloads::Single(first_overload); + return CallableSignature::Single(first_overload); }; let mut overloads = vec![first_overload, second_overload]; overloads.extend(iter); - Overloads::Overloaded(overloads.into()) + CallableSignature::Overloaded(overloads.into()) } pub(crate) fn iter(&self) -> std::slice::Iter> { match self { - Overloads::Single(signature) => std::slice::from_ref(signature).iter(), - Overloads::Overloaded(signatures) => signatures.iter(), + CallableSignature::Single(signature) => std::slice::from_ref(signature).iter(), + CallableSignature::Overloaded(signatures) => signatures.iter(), } } @@ -55,14 +68,13 @@ impl<'db> Overloads<'db> { } } -impl<'db> From> for Overloads<'db> { +impl<'db> From> for CallableSignature<'db> { fn from(signature: Signature<'db>) -> Self { - Overloads::Single(signature) + CallableSignature::Single(signature) } } -/// A typed callable signature. If a callable is overloaded, there will be one of these for each -/// possible overload. +/// The signature of one of the overloads of a callable. #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub struct Signature<'db> { /// Parameters, in source order. @@ -785,7 +797,7 @@ mod tests { .unwrap(); let func = get_function_f(&db, "/src/a.py"); - let expected_sig = Overloads::todo("return type of decorated function"); + let expected_sig = CallableSignature::todo("return type of decorated function"); // With no decorators, internal and external signature are the same assert_eq!(func.signature(&db), &expected_sig); From e04f15c46ca661a81d943ab095f72e44e19f6997 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 11 Mar 2025 14:58:38 -0400 Subject: [PATCH 32/32] Fix merge conflicts --- crates/red_knot_python_semantic/src/types.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 75ff9e36552db..64520da736704 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2815,8 +2815,7 @@ impl<'db> Type<'db> { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { .. }) => { - let signature = - Signature::new(Parameters::gradual_form(), Some(self.to_instance(db))); + let signature = Signature::new(Parameters::gradual_form(), self.to_instance(db)); let binding = bind_call(db, arguments, &signature.into(), self); binding.into_outcome() }