diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 047a45fc2f..7aa76be99c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -118,4 +118,126 @@ class R: ... static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R])) ``` +## Callable + +### Equivalent + +For an equivalence relationship, the default value does not necessarily need to be the same but if +the parameter in one of the callable has a default value then the corresponding parameter in the +other callable should also have a default value. + +```py +from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert +from typing import Callable + +def f1(a: int = 1) -> None: ... +def f2(a: int = 2) -> None: ... + +static_assert(is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2])) +``` + +The names of the positional-only, variadic and keyword-variadic parameters does not need to be the +same. + +```py +def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ... +def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ... + +static_assert(is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4])) +``` + +Putting it all together, the following two callables are equivalent: + +```py +def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ... +def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ... + +static_assert(is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6])) +``` + +### Not equivalent + +There are multiple cases when two callable types are not equivalent which are enumerated below. + +```py +from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert +from typing import Callable +``` + +When the number of parameters is different: + +```py +def f1(a: int) -> None: ... +def f2(a: int, b: int) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2])) +``` + +When either of the callable types uses a gradual form for the parameters: + +```py +static_assert(not is_equivalent_to(Callable[..., None], Callable[[int], None])) +static_assert(not is_equivalent_to(Callable[[int], None], Callable[..., None])) +``` + +When the return types are not equivalent or absent in one or both of the callable types: + +```py +def f3(): ... +def f4() -> None: ... + +static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f3])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f4], CallableTypeFromFunction[f3])) +``` + +When the parameter names are different: + +```py +def f5(a: int) -> None: ... +def f6(b: int) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6])) +``` + +When only one of the callable types has parameter names: + +```py +static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], Callable[[int], None])) +``` + +When the parameter kinds are different: + +```py +def f7(a: int, /) -> None: ... +def f8(a: int) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeFromFunction[f7], CallableTypeFromFunction[f8])) +``` + +When the annotated types of the parameters are not equivalent or absent in one or both of the +callable types: + +```py +def f9(a: int) -> None: ... +def f10(a: str) -> None: ... +def f11(a) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeFromFunction[f9], CallableTypeFromFunction[f10])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f10], CallableTypeFromFunction[f11])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f10])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f11])) +``` + +When the default value for a parameter is present only in one of the callable type: + +```py +def f12(a: int) -> None: ... +def f13(a: int = 2) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeFromFunction[f12], CallableTypeFromFunction[f13])) +static_assert(not is_equivalent_to(CallableTypeFromFunction[f13], CallableTypeFromFunction[f12])) +``` + [the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1deeb0e08e..4ce91400d7 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -898,6 +898,10 @@ impl<'db> Type<'db> { left.is_equivalent_to(db, right) } (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), + ( + Type::Callable(CallableType::General(left)), + Type::Callable(CallableType::General(right)), + ) => left.is_equivalent_to(db, right), _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), } } @@ -4362,10 +4366,8 @@ impl<'db> FunctionType<'db> { /// Convert the `FunctionType` into a [`Type::Callable`]. /// - /// Returns `None` if the function is overloaded. This powers the `CallableTypeFromFunction` - /// special form from the `knot_extensions` module. + /// This powers the `CallableTypeFromFunction` special form from the `knot_extensions` module. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - // TODO: Add support for overloaded callables Type::Callable(CallableType::General(GeneralCallableType::new( db, self.signature(db).clone(), @@ -4611,6 +4613,90 @@ impl<'db> GeneralCallableType<'db> { .is_some_and(|return_type| return_type.is_fully_static(db)) } + /// Return `true` if `self` represents the exact same set of possible runtime objects as `other`. + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + let self_signature = self.signature(db); + let other_signature = other.signature(db); + + let self_parameters = self_signature.parameters(); + let other_parameters = other_signature.parameters(); + + if self_parameters.len() != other_parameters.len() { + return false; + } + + if self_parameters.is_gradual() || other_parameters.is_gradual() { + return false; + } + + // Check equivalence relationship between two optional types. If either of them is `None`, + // then it is not a fully static type which means it's not equivalent either. + let is_equivalent = |self_type: Option>, other_type: Option>| match ( + self_type, other_type, + ) { + (Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type), + _ => false, + }; + + if !is_equivalent(self_signature.return_ty, other_signature.return_ty) { + return false; + } + + for (self_parameter, other_parameter) in self_parameters.iter().zip(other_parameters) { + match (self_parameter.kind(), other_parameter.kind()) { + ( + ParameterKind::PositionalOnly { + default_ty: self_default, + .. + }, + ParameterKind::PositionalOnly { + default_ty: other_default, + .. + }, + ) if self_default.is_some() == other_default.is_some() => {} + + ( + ParameterKind::PositionalOrKeyword { + name: self_name, + default_ty: self_default, + }, + ParameterKind::PositionalOrKeyword { + name: other_name, + default_ty: other_default, + }, + ) if self_default.is_some() == other_default.is_some() + && self_name == other_name => {} + + (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {} + + ( + ParameterKind::KeywordOnly { + name: self_name, + default_ty: self_default, + }, + ParameterKind::KeywordOnly { + name: other_name, + default_ty: other_default, + }, + ) if self_default.is_some() == other_default.is_some() + && self_name == other_name => {} + + (ParameterKind::KeywordVariadic { .. }, ParameterKind::KeywordVariadic { .. }) => {} + + _ => return false, + } + + if !is_equivalent( + self_parameter.annotated_type(), + other_parameter.annotated_type(), + ) { + return false; + } + } + + true + } + /// Return `true` if `self` has exactly the same set of possible static materializations as /// `other` (if `self` represents the same set of possible sets of possible runtime objects as /// `other`). diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 3590c72ac0..d4324d271c 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -601,6 +601,11 @@ impl<'db> Parameter<'db> { self.annotated_ty } + /// Kind of the parameter. + pub(crate) fn kind(&self) -> &ParameterKind<'db> { + &self.kind + } + /// Name of the parameter (if it has one). pub(crate) fn name(&self) -> Option<&ast::name::Name> { match &self.kind {