[red-knot] Check subtype relation between callable types (#16804)

## Summary

Part of #15382

This PR adds support for checking the subtype relationship between the
two callable types.

The main source of reference used for implementation is
https://typing.python.org/en/latest/spec/callables.html#assignability-rules-for-callables.

The implementation is split into two phases:
1. Check all the positional parameters which includes positional-only,
standard (positional or keyword) and variadic kind
2. Collect all the keywords in a `HashMap` to do the keyword parameters
check via name lookup

For (1), there's a helper struct which is similar to `.zip_longest`
(from `itertools`) except that it allows control over one of the
iterator as that's required when processing a variadic parameter. This
is required because positional parameters needs to be checked as per
their position between the two callable types. The struct also keeps
track of the current iteration element because when the loop is exited
(to move on to the phase 2) the current iteration element would be
carried over to the phase 2 check.

This struct is internal to the `is_subtype_of` method as I don't think
it makes sense to expose it outside. It also allows me to use "self" and
"other" suffixed field names as that's only relevant in that context.

## Test Plan

Add extensive tests in markdown.

Converted all of the code snippets from
https://typing.python.org/en/latest/spec/callables.html#assignability-rules-for-callables
to use `knot_extensions.is_subtype_of` and verified the result.
This commit is contained in:
Dhruv Manilawala 2025-03-21 08:57:22 +05:30 committed by GitHub
parent 193c38199e
commit 04a8756379
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 856 additions and 2 deletions

View file

@ -478,5 +478,515 @@ static_assert(not is_subtype_of(Intersection[Unknown, int], int))
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown]))
```
## Callable
The general principle is that a callable type is a subtype of another if it's more flexible in what
it accepts and more specific in what it returns.
References:
- <https://typing.python.org/en/latest/spec/callables.html#assignability-rules-for-callables>
- <https://typing.python.org/en/latest/spec/callables.html#assignment>
### Return type
Return types are covariant.
```py
from typing import Callable
from knot_extensions import is_subtype_of, static_assert
static_assert(is_subtype_of(Callable[[], int], Callable[[], float]))
static_assert(not is_subtype_of(Callable[[], float], Callable[[], int]))
```
### Parameter types
Parameter types are contravariant.
#### Positional-only
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def float_param(a: float, /) -> None: ...
def int_param(a: int, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
```
Parameter name is not required to be the same for positional-only parameters at the same position:
```py
def int_param_different_name(b: int, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_param_different_name]))
static_assert(is_subtype_of(CallableTypeFromFunction[int_param_different_name], CallableTypeFromFunction[int_param]))
```
Multiple positional-only parameters are checked in order:
```py
def multi_param1(a: float, b: int, c: str, /) -> None: ...
def multi_param2(b: int, c: bool, a: str, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
```
#### Positional-only with default value
If the parameter has a default value, it's treated as optional. This means that the parameter at the
corresponding position in the supertype does not need to have a default value.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def float_with_default(a: float = 1, /) -> None: ...
def int_with_default(a: int = 1, /) -> None: ...
def int_without_default(a: int, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
```
As the parameter itself is optional, it can be omitted in the supertype:
```py
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
```
The subtype can include any number of positional-only parameters as long as they have the default
value:
```py
def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param]))
```
#### Positional-only with other kinds
If a parameter is declared as positional-only, then the corresponding parameter in the supertype
cannot be any other parameter kind.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def positional_only(a: int, /) -> None: ...
def standard(a: int) -> None: ...
def keyword_only(*, a: int) -> None: ...
def variadic(*a: int) -> None: ...
def keyword_variadic(**a: int) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[standard]))
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_only]))
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[variadic]))
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_variadic]))
```
#### Standard
A standard parameter is either a positional or a keyword parameter.
Unlike positional-only parameters, standard parameters should have the same name in the subtype.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def int_param_a(a: int) -> None: ...
def int_param_b(b: int) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_a], CallableTypeFromFunction[int_param_b]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_b], CallableTypeFromFunction[int_param_a]))
```
Apart from the name, it behaves the same as positional-only parameters.
```py
def float_param(a: float) -> None: ...
def int_param(a: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
```
With the same rules for default values as well.
```py
def float_with_default(a: float = 1) -> None: ...
def int_with_default(a: int = 1) -> None: ...
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_param]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
```
Multiple standard parameters are checked in order along with their names:
```py
def multi_param1(a: float, b: int, c: str) -> None: ...
def multi_param2(a: int, b: bool, c: str) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
```
The subtype can include as many standard parameters as long as they have the default value:
```py
def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param_default], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param_default]))
```
#### Standard with keyword-only
A keyword-only parameter in the supertype can be substituted with the corresponding standard
parameter in the subtype with the same name. This is because a standard parameter is more flexible
than a keyword-only parameter.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def standard_a(a: int) -> None: ...
def keyword_b(*, b: int) -> None: ...
# The name of the parameters are different
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[keyword_b]))
def standard_float(a: float) -> None: ...
def keyword_int(*, a: int) -> None: ...
# Here, the name of the parameters are the same
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[keyword_int]))
def standard_with_default(a: int = 1) -> None: ...
def keyword_with_default(*, a: int = 1) -> None: ...
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keyword_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
```
The position of the keyword-only parameters does not matter:
```py
def multi_standard(a: float, b: int, c: str) -> None: ...
def multi_keyword(*, b: bool, c: str, a: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_keyword]))
```
#### Standard with positional-only
A positional-only parameter in the supertype can be substituted with the corresponding standard
parameter in the subtype at the same position. This is because a standard parameter is more flexible
than a positional-only parameter.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def standard_a(a: int) -> None: ...
def positional_b(b: int, /) -> None: ...
# The names are not important in this context
static_assert(is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[positional_b]))
def standard_float(a: float) -> None: ...
def positional_int(a: int, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[positional_int]))
def standard_with_default(a: int = 1) -> None: ...
def positional_with_default(a: int = 1, /) -> None: ...
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[positional_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
```
The position of the positional-only parameters matter:
```py
def multi_standard(a: float, b: int, c: str) -> None: ...
def multi_positional1(b: int, c: bool, a: str, /) -> None: ...
# Here, the type of the parameter `a` makes the subtype relation invalid
def multi_positional2(b: int, a: float, c: str, /) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional1]))
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional2]))
```
#### Standard with variadic
A variadic or keyword-variadic parameter in the supertype cannot be substituted with a standard
parameter in the subtype.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def standard(a: int) -> None: ...
def variadic(*a: int) -> None: ...
def keyword_variadic(**a: int) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[variadic]))
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keyword_variadic]))
```
#### Variadic
The name of the variadic parameter does not need to be the same in the subtype.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def variadic_float(*args2: float) -> None: ...
def variadic_int(*args1: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_float], CallableTypeFromFunction[variadic_int]))
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[variadic_float]))
```
The variadic parameter does not need to be present in the supertype:
```py
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[variadic_int]))
```
#### Variadic with positional-only
If the subtype has a variadic parameter then any unmatched positional-only parameter from the
supertype should be checked against the variadic parameter.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def variadic(a: int, /, *args: float) -> None: ...
# Here, the parameter `b` and `c` are unmatched
def positional_only(a: int, b: float, c: int, /) -> None: ...
# Here, the parameter `b` is unmatched and there's also a variadic parameter
def positional_variadic(a: int, b: float, /, *args: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_only]))
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_variadic]))
```
#### Variadic with other kinds
Variadic parameter in a subtype can only be used to match against an unmatched positional-only
parameters from the supertype, not any other parameter kind.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def variadic(*args: int) -> None: ...
# Both positional-only parameters are unmatched so uses the variadic parameter but the other
# parameter `c` remains and cannot be matched.
def standard(a: int, b: float, /, c: int) -> None: ...
# Similarly, for other kinds
def keyword_only(a: int, /, *, b: int) -> None: ...
def keyword_variadic(a: int, /, **kwargs: int) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[standard]))
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_only]))
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_variadic]))
```
#### Keyword-only
For keyword-only parameters, the name should be the same:
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def keyword_int(*, a: int) -> None: ...
def keyword_float(*, a: float) -> None: ...
def keyword_b(*, b: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_float], CallableTypeFromFunction[keyword_int]))
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_float]))
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_b]))
```
But, the order of the keyword-only parameters is not required to be the same:
```py
def keyword_ab(*, a: float, b: float) -> None: ...
def keyword_ba(*, b: int, a: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_ab], CallableTypeFromFunction[keyword_ba]))
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_ba], CallableTypeFromFunction[keyword_ab]))
```
#### Keyword-only with default
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def float_with_default(*, a: float = 1) -> None: ...
def int_with_default(*, a: int = 1) -> None: ...
def int_keyword(*, a: int) -> None: ...
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_keyword]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[int_with_default]))
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
```
Keyword-only parameters with default values can be mixed with the ones without default values in any
order:
```py
# A keyword-only parameter with a default value follows the one without a default value (it's valid)
def mixed(*, b: int = 1, a: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[int_keyword]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[mixed]))
```
#### Keyword-only with standard
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def keywords1(*, a: int, b: int) -> None: ...
def standard(b: float, a: float) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords1], CallableTypeFromFunction[standard]))
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keywords1]))
```
The subtype can include additional standard parameters as long as it has the default value:
```py
def standard_with_default(b: float, a: float, c: float = 1) -> None: ...
def standard_without_default(b: float, a: float, c: float) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_without_default], CallableTypeFromFunction[keywords1]))
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keywords1]))
```
Here, we mix keyword-only parameters with standard parameters:
```py
def keywords2(*, a: int, c: int, b: int) -> None: ...
def mixed(b: float, a: float, *, c: float) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords2], CallableTypeFromFunction[mixed]))
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[keywords2]))
```
But, we shouldn't consider any unmatched positional-only parameters:
```py
def mixed_positional(b: float, /, a: float, *, c: float) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[mixed_positional], CallableTypeFromFunction[keywords2]))
```
But, an unmatched variadic parameter is still valid:
```py
def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[mixed_variadic], CallableTypeFromFunction[keywords2]))
```
#### Keyword-variadic
The name of the keyword-variadic parameter does not need to be the same in the subtype.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def kwargs_float(**kwargs2: float) -> None: ...
def kwargs_int(**kwargs1: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_float], CallableTypeFromFunction[kwargs_int]))
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[kwargs_float]))
```
A variadic parameter can be omitted in the subtype:
```py
def empty() -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[kwargs_int]))
```
#### Keyword-variadic with keyword-only
If the subtype has a keyword-variadic parameter then any unmatched keyword-only parameter from the
supertype should be checked against the keyword-variadic parameter.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def kwargs(**kwargs: float) -> None: ...
def keyword_only(*, a: int, b: float, c: bool) -> None: ...
def keyword_variadic(*, a: int, **kwargs: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_only]))
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_variadic]))
```
This is valid only for keyword-only parameters, not any other parameter kind:
```py
def mixed1(a: int, *, b: int) -> None: ...
# Same as above but with the default value
def mixed2(a: int = 1, *, b: int) -> None: ...
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed1]))
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed2]))
```
#### Empty
When the supertype has an empty list of parameters, then the subtype can have any kind of parameters
as long as they contain the default values for non-variadic parameters.
```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
def empty() -> None: ...
def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ...
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[empty]))
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[mixed]))
```
[special case for float and complex]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence

View file

@ -1,10 +1,12 @@
use std::hash::Hash;
use std::collections::HashMap;
use std::slice::Iter;
use std::str::FromStr;
use bitflags::bitflags;
use call::{CallDunderError, CallError, CallErrorKind};
use context::InferContext;
use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
use itertools::EitherOrBoth;
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
@ -654,8 +656,14 @@ impl<'db> Type<'db> {
.is_subtype_of(db, target)
}
(
Type::Callable(CallableType::General(self_callable)),
Type::Callable(CallableType::General(other_callable)),
) => self_callable.is_subtype_of(db, other_callable),
(Type::Callable(CallableType::General(_)), _) => {
// TODO: Implement subtyping for general callable types
// TODO: Implement subtyping between general callable types and other types like
// function literals, bound methods, class literals, `type[]`, etc.)
false
}
@ -4738,6 +4746,342 @@ impl<'db> GeneralCallableType<'db> {
)
})
}
/// Return `true` if `self` is a subtype of `other`.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
/// A helper struct to zip two slices of parameters together that provides control over the
/// two iterators individually. It also keeps track of the current parameter in each
/// iterator.
struct ParametersZip<'a, 'db> {
current_self: Option<&'a Parameter<'db>>,
current_other: Option<&'a Parameter<'db>>,
iter_self: Iter<'a, Parameter<'db>>,
iter_other: Iter<'a, Parameter<'db>>,
}
impl<'a, 'db> ParametersZip<'a, 'db> {
/// Move to the next parameter in both the `self` and `other` parameter iterators,
/// [`None`] if both iterators are exhausted.
fn next(&mut self) -> Option<EitherOrBoth<&'a Parameter<'db>, &'a Parameter<'db>>> {
match (self.next_self(), self.next_other()) {
(Some(self_param), Some(other_param)) => {
Some(EitherOrBoth::Both(self_param, other_param))
}
(Some(self_param), None) => Some(EitherOrBoth::Left(self_param)),
(None, Some(other_param)) => Some(EitherOrBoth::Right(other_param)),
(None, None) => None,
}
}
/// Move to the next parameter in the `self` parameter iterator, [`None`] if the
/// iterator is exhausted.
fn next_self(&mut self) -> Option<&'a Parameter<'db>> {
self.current_self = self.iter_self.next();
self.current_self
}
/// Move to the next parameter in the `other` parameter iterator, [`None`] if the
/// iterator is exhausted.
fn next_other(&mut self) -> Option<&'a Parameter<'db>> {
self.current_other = self.iter_other.next();
self.current_other
}
/// Peek at the next parameter in the `other` parameter iterator without consuming it.
fn peek_other(&mut self) -> Option<&'a Parameter<'db>> {
self.iter_other.clone().next()
}
/// Consumes the `ParametersZip` and returns a two-element tuple containing the
/// remaining parameters in the `self` and `other` iterators respectively.
///
/// The returned iterators starts with the current parameter, if any, followed by the
/// remaining parameters in the respective iterators.
fn into_remaining(
self,
) -> (
impl Iterator<Item = &'a Parameter<'db>>,
impl Iterator<Item = &'a Parameter<'db>>,
) {
(
self.current_self.into_iter().chain(self.iter_self),
self.current_other.into_iter().chain(self.iter_other),
)
}
}
let self_signature = self.signature(db);
let other_signature = other.signature(db);
// Check if `type1` is a subtype of `type2`. This is mainly to avoid `unwrap` calls
// scattered throughout the function.
let is_subtype = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
// SAFETY: Subtype relation is only checked for fully static types.
type1.unwrap().is_subtype_of(db, type2.unwrap())
};
// Return types are covariant.
if !is_subtype(self_signature.return_ty, other_signature.return_ty) {
return false;
}
let mut parameters = ParametersZip {
current_self: None,
current_other: None,
iter_self: self_signature.parameters().iter(),
iter_other: other_signature.parameters().iter(),
};
loop {
let Some(next_parameter) = parameters.next() else {
// All parameters have been checked or both the parameter lists were empty. In
// either case, `self` is a subtype of `other`.
return true;
};
match next_parameter {
EitherOrBoth::Left(self_parameter) => match self_parameter.kind() {
ParameterKind::PositionalOnly { default_ty, .. }
| ParameterKind::PositionalOrKeyword { default_ty, .. }
| ParameterKind::KeywordOnly { default_ty, .. } => {
// For `self <: other` to be valid, if there are no more parameters in
// `other`, then the non-variadic parameters in `self` must have a default
// value.
if default_ty.is_none() {
return false;
}
}
ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => {
// Variadic parameters don't have any restrictions in this context, so
// we'll just continue to the next parameter set.
}
},
EitherOrBoth::Right(_) => {
// If there are more parameters in `other` than in `self`, then `self` is not a
// subtype of `other`.
return false;
}
EitherOrBoth::Both(self_parameter, other_parameter) => {
match (self_parameter.kind(), other_parameter.kind()) {
(
ParameterKind::PositionalOnly {
default_ty: self_default,
..
}
| ParameterKind::PositionalOrKeyword {
default_ty: self_default,
..
},
ParameterKind::PositionalOnly {
default_ty: other_default,
..
},
) => {
if self_default.is_none() && other_default.is_some() {
return false;
}
if !is_subtype(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}
}
(
ParameterKind::PositionalOrKeyword {
name: self_name,
default_ty: self_default,
},
ParameterKind::PositionalOrKeyword {
name: other_name,
default_ty: other_default,
},
) => {
if self_name != other_name {
return false;
}
// The following checks are the same as positional-only parameters.
if self_default.is_none() && other_default.is_some() {
return false;
}
if !is_subtype(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}
}
(ParameterKind::Variadic { .. }, ParameterKind::PositionalOnly { .. }) => {
if !is_subtype(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}
// We've reached a variadic parameter in `self` which means there can
// be no more positional parameters after this in a valid AST. But, the
// current parameter in `other` is a positional-only which means there
// can be more positional parameters after this which could be either
// more positional-only parameters, standard parameters or a variadic
// parameter.
//
// So, any remaining positional parameters in `other` would need to be
// checked against the variadic parameter in `self`. This loop does
// that by only moving the `other` iterator forward.
loop {
let Some(other_parameter) = parameters.peek_other() else {
break;
};
if !matches!(
other_parameter.kind(),
ParameterKind::PositionalOnly { .. }
| ParameterKind::Variadic { .. }
) {
// Any other parameter kind cannot be checked against a
// variadic parameter and is deferred to the next iteration.
break;
}
if !is_subtype(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}
parameters.next_other();
}
}
(ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {
if !is_subtype(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}
}
(
_,
ParameterKind::KeywordOnly { .. }
| ParameterKind::KeywordVariadic { .. },
) => {
// Keyword parameters are not considered in this loop as the order of
// parameters is not important for them and so they are checked by
// doing name-based lookups.
break;
}
_ => return false,
}
}
}
}
// At this point, the remaining parameters in `other` are keyword-only or keyword variadic.
// But, `self` could contain any unmatched positional parameters.
let (self_parameters, other_parameters) = parameters.into_remaining();
// Collect all the keyword-only parameters and the unmatched standard parameters.
let mut self_keywords = HashMap::new();
// Type of the variadic keyword parameter in `self`.
//
// This is a nested option where the outer option represents the presence of a keyword
// variadic parameter in `self` and the inner option represents the annotated type of the
// keyword variadic parameter.
let mut self_keyword_variadic: Option<Option<Type<'db>>> = None;
for self_parameter in self_parameters {
match self_parameter.kind() {
ParameterKind::KeywordOnly { name, .. }
| ParameterKind::PositionalOrKeyword { name, .. } => {
self_keywords.insert(name.clone(), self_parameter);
}
ParameterKind::KeywordVariadic { .. } => {
self_keyword_variadic = Some(self_parameter.annotated_type());
}
ParameterKind::PositionalOnly { .. } => {
// These are the unmatched positional-only parameters in `self` from the
// previous loop. They cannot be matched against any parameter in `other` which
// only contains keyword-only and keyword-variadic parameters so the subtype
// relation is invalid.
return false;
}
ParameterKind::Variadic { .. } => {}
}
}
for other_parameter in other_parameters {
match other_parameter.kind() {
ParameterKind::KeywordOnly {
name: other_name,
default_ty: other_default,
} => {
if let Some(self_parameter) = self_keywords.remove(other_name) {
match self_parameter.kind() {
ParameterKind::PositionalOrKeyword {
default_ty: self_default,
..
}
| ParameterKind::KeywordOnly {
default_ty: self_default,
..
} => {
if self_default.is_none() && other_default.is_some() {
return false;
}
if !is_subtype(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}
}
_ => unreachable!(
"`self_keywords` should only contain keyword-only or standard parameters"
),
}
} else if let Some(self_keyword_variadic_type) = self_keyword_variadic {
if !is_subtype(other_parameter.annotated_type(), self_keyword_variadic_type)
{
return false;
}
} else {
return false;
}
}
ParameterKind::KeywordVariadic { .. } => {
let Some(self_keyword_variadic_type) = self_keyword_variadic else {
// For a `self <: other` relationship, if `other` has a keyword variadic
// parameter, `self` must also have a keyword variadic parameter.
return false;
};
if !is_subtype(other_parameter.annotated_type(), self_keyword_variadic_type) {
return false;
}
}
_ => {
// This can only occur in case of a syntax error.
return false;
}
}
}
// If there are still unmatched keyword parameters from `self`, then they should be
// optional otherwise the subtype relation is invalid.
for (_, self_parameter) in self_keywords {
if self_parameter.default_type().is_none() {
return false;
}
}
true
}
}
/// A type that represents callable objects.