diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md index fce0395bde..a45687d4ad 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md @@ -68,6 +68,10 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str])) ## Callable +The examples provided below are only a subset of the possible cases and only include the ones with +gradual types. The cases with fully static types and using different combinations of parameter kinds +are covered in the [equivalence tests](./is_equivalent_to.md#callable). + ```py from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert from typing import Any, Callable @@ -94,7 +98,7 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[] And, similarly for parameters with no annotations. ```py -def f2(a, b) -> None: +def f2(a, b, /) -> None: return static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None])) @@ -115,8 +119,8 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any])) ``` -But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...` -as the parameter type. +But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a +callable with `...` as the parameter type. ```py def variadic_args(*args): @@ -129,4 +133,25 @@ static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_arg static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any])) ``` +Parameter names, default values, and it's kind should also be considered when checking for gradual +equivalence. + +```py +def f1(a): ... +def f2(b): ... + +static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2])) + +def f3(a=1): ... +def f4(a=2): ... +def f5(a): ... + +static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4])) +static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5])) + +def f6(a, /): ... + +static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6])) +``` + [materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f1a744b8a4..dea4ecd8d7 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4346,11 +4346,44 @@ impl<'db> GeneralCallableType<'db> { .is_some_and(|return_type| return_type.is_fully_static(db)) } + /// 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`). + pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.is_equivalent_to_impl(db, other, |self_type, other_type| { + self_type + .unwrap_or(Type::unknown()) + .is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown())) + }) + } + /// 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 { + self.is_equivalent_to_impl(db, other, |self_type, other_type| { + match (self_type, other_type) { + (Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type), + // We need the catch-all case here because it's not guaranteed that this is a fully + // static type. + _ => false, + } + }) + } + + /// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for callable + /// types. + /// + /// [`is_equivalent_to`]: Self::is_equivalent_to + /// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to + fn is_equivalent_to_impl(self, db: &'db dyn Db, other: Self, check_types: F) -> bool + where + F: Fn(Option>, Option>) -> bool, + { let self_signature = self.signature(db); let other_signature = other.signature(db); + // N.B. We don't need to explicitly check for the use of gradual form (`...`) in the + // parameters because it is internally represented by adding `*Any` and `**Any` to the + // parameter list. let self_parameters = self_signature.parameters(); let other_parameters = other_signature.parameters(); @@ -4358,20 +4391,7 @@ impl<'db> GeneralCallableType<'db> { 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) { + if !check_types(self_signature.return_ty, other_signature.return_ty) { return false; } @@ -4419,7 +4439,7 @@ impl<'db> GeneralCallableType<'db> { _ => return false, } - if !is_equivalent( + if !check_types( self_parameter.annotated_type(), other_parameter.annotated_type(), ) { @@ -4430,48 +4450,6 @@ impl<'db> GeneralCallableType<'db> { 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`). - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - let self_signature = self.signature(db); - let other_signature = other.signature(db); - - if self_signature.parameters().len() != other_signature.parameters().len() { - return false; - } - - // Check gradual equivalence between the two optional types. In the context of a callable - // type, the `None` type represents an `Unknown` type. - let are_optional_types_gradually_equivalent = - |self_type: Option>, other_type: Option>| { - self_type - .unwrap_or(Type::unknown()) - .is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown())) - }; - - if !are_optional_types_gradually_equivalent( - self_signature.return_ty, - other_signature.return_ty, - ) { - return false; - } - - // N.B. We don't need to explicitly check for the use of gradual form (`...`) in the - // parameters because it is internally represented by adding `*Any` and `**Any` to the - // parameter list. - self_signature - .parameters() - .iter() - .zip(other_signature.parameters().iter()) - .all(|(self_param, other_param)| { - are_optional_types_gradually_equivalent( - self_param.annotated_type(), - other_param.annotated_type(), - ) - }) - } - /// Return `true` if `self` is assignable to `other`. pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { self.is_assignable_to_impl(db, other, |type1, type2| { @@ -4483,6 +4461,10 @@ impl<'db> GeneralCallableType<'db> { } /// Return `true` if `self` is a subtype of `other`. + /// + /// # Panics + /// + /// Panics if `self` or `other` is not a fully static type. pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { self.is_assignable_to_impl(db, other, |type1, type2| { // SAFETY: Subtype relation is only checked for fully static types.