Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 21, 2025

Conversation

dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented Mar 17, 2025

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.

@dhruvmanila dhruvmanila added the ty Multi-file analysis & type inference label Mar 17, 2025
@dhruvmanila dhruvmanila changed the title [WIP] [red-knot] Fix fully static check for callable type [WIP] [red-knot] Check subtype relation between callable types Mar 17, 2025
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-static-fix branch from e6e65d7 to 6192776 Compare March 17, 2025 14:24
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-subtype branch from 8e60a84 to eb8f3ca Compare March 17, 2025 14:25
Copy link
Contributor

github-actions bot commented Mar 17, 2025

mypy_primer results

No ecosystem changes detected ✅

Base automatically changed from dhruv/callable-static-fix to main March 17, 2025 14:31
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-subtype branch from eb8f3ca to f9ee155 Compare March 17, 2025 14:31
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good! I don't have any significant advice based on what's here already; happy to consider any specific problems you're having with completing the PR.

I think that when we add assignability for callable types, it will make most sense to just expand this function to handle gradual types also and rename it is_assignable_to. Then the function can work both for is_subtype_of and is_assignable_to, in the former case it will just never receive a non-fully-static callable type.

Copy link
Contributor

github-actions bot commented Mar 18, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@dhruvmanila
Copy link
Member Author

I think that when we add assignability for callable types, it will make most sense to just expand this function to handle gradual types also and rename it is_assignable_to. Then the function can work both for is_subtype_of and is_assignable_to, in the former case it will just never receive a non-fully-static callable type.

Yeah, I think that makes sense. For now, I did make the assumption that we're only getting fully static types so I avoided the fallback to Unknown if there is no annotated type, mainly to avoid any surprise behavior but that shouldn't be too difficult to change to unwrap_or(Type::unknown()). And, if the entire parameter type is using a gradual form (...) then we can directly short-circuit the implementation.

@dhruvmanila dhruvmanila force-pushed the dhruv/callable-subtype branch from 12dc9eb to 98da221 Compare March 19, 2025 04:44
@dhruvmanila dhruvmanila changed the title [WIP] [red-knot] Check subtype relation between callable types [red-knot] Check subtype relation between callable types Mar 19, 2025
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-subtype branch from 98da221 to 79a4afc Compare March 19, 2025 10:50
@dhruvmanila dhruvmanila changed the base branch from main to dhruv/callable-is-equivalent March 19, 2025 10:50
@dhruvmanila dhruvmanila marked this pull request as ready for review March 19, 2025 10:54
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good!! Quite a complex feature, thanks for working through it so thoroughly.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that argument lists will tend to be short enough that it will be cheaper to use a Vec and just iterate through it than to pay the overhead of hashing. But I'm fine leaving this for a future performance investigation if we see this code as a hot spot. A mapping is semantically the appropriate data structure here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, another option is to use smallmap crate.

@dhruvmanila dhruvmanila force-pushed the dhruv/callable-is-equivalent branch from dfe5fcc to 6a1f150 Compare March 20, 2025 12:10
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-subtype branch from 12ff6b9 to a519c2e Compare March 20, 2025 12:10
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really excellent!!

Base automatically changed from dhruv/callable-is-equivalent to main March 21, 2025 03:19
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-subtype branch from a519c2e to 3b1e030 Compare March 21, 2025 03:23
@dhruvmanila dhruvmanila enabled auto-merge (squash) March 21, 2025 03:23
@dhruvmanila dhruvmanila merged commit 04a8756 into main Mar 21, 2025
22 checks passed
@dhruvmanila dhruvmanila deleted the dhruv/callable-subtype branch March 21, 2025 03:27
dcreager added a commit that referenced this pull request Mar 21, 2025
* main: (26 commits)
  Use the common `OperatorPrecedence` for the parser (#16747)
  [red-knot] Check subtype relation between callable types (#16804)
  [red-knot] Check whether two callable types are equivalent (#16698)
  [red-knot] Ban most `Type::Instance` types in type expressions (#16872)
  Special-case value-expression inference of special form subscriptions (#16877)
  [syntax-errors] Fix star annotation before Python 3.11 (#16878)
  Recognize `SyntaxError:` as an error code for ecosystem checks (#16879)
  [red-knot] add test cases result in false positive errors (#16856)
  Bump 0.11.1 (#16871)
  Allow discovery of venv in VIRTUAL_ENV env variable (#16853)
  Split git pathspecs in change determination onto separate lines (#16869)
  Use the correct base commit for change determination (#16857)
  Separate `BitXorOr` into `BitXor` and `BitOr` precedence (#16844)
  Server: Allow `FixAll` action in presence of version-specific syntax errors (#16848)
  [`refurb`] Fix starred expressions fix (`FURB161`) (#16550)
  [`flake8-executable`] Add pytest and uv run to help message for `shebang-missing-python` (`EXE003`) (#16855)
  Show more precise messages in invalid type expressions (#16850)
  [`flake8-executables`] Allow `uv run` in shebang line for `shebang-missing-python` (`EXE003`) (#16849)
  Add `--exit-non-zero-on-format` (#16009)
  [red-knot] Ban list literals in most contexts in type expressions (#16847)
  ...
dhruvmanila added a commit that referenced this pull request Mar 22, 2025
## Summary

Part of #15382

This PR adds support for checking the assignability of two general
callable types.

This is built on top of #16804 by including the gradual parameters check
and accepting a function that performs the check between the two types.

## Test Plan

Update `is_assignable_to.md` with callable types section.
@sharkdp
Copy link
Contributor

sharkdp commented Mar 24, 2025

Very nice work! One minor question: should Callables be a subtype of object? Should this pass?

from knot_extensions import static_assert, is_subtype_of
from typing import Callable

static_assert(is_subtype_of(Callable[[int], str], object))

https://playknot.ruff.rs/2fd03ad6-c4d9-45e5-bde5-8f2d1ec55abc

@dhruvmanila
Copy link
Member Author

One minor question: should Callables be a subtype of object? Should this pass?

Hmm, good question. I think not because object is not callable.

@AlexWaygood
Copy link
Member

AlexWaygood commented Mar 24, 2025

Hmm, good question. I think not because object is not callable.

That's opposite to the way you should think about it -- all fully static types should be a subtype of object.

Consider the way that all instance types are a subtype of object. For example, the bool type extends the features and properties of object (it is an (indirect) subtype of object); thus bool is a subtype of object even though not all instances of object are instances of bool.

@carljm
Copy link
Contributor

carljm commented Mar 24, 2025

Callables (fully static ones) should be a subtype of object. The type object is inhabited by all Python objects, so all (fully static) types are subtypes of object.

It is true that all objects are not callable, but this means that object is not a subtype of Callable.

In my understanding this PR wasn't aiming to be comprehensive yet, it was just handling the core calculation of signature subtyping. We have other relations missing as well, eg a FunctionLiteral can be a subtype of a Callable type. Also an Instance type with a __call__ method. And a ClassLiteral type (based on its constructor signature.) And arguably SubclassOf as well, though this one is not sound since we won't enforce Liskov on constructor signatures.

@dhruvmanila
Copy link
Member Author

dhruvmanila commented Mar 24, 2025

Callables (fully static ones) should be a subtype of object. The type object is inhabited by all Python objects, so all (fully static) types are subtypes of object.

Yeah, I think I got it backwards, thanks.

In my understanding this PR wasn't aiming to be comprehensive yet, it was just handling the core calculation of signature subtyping. We have other relations missing as well, eg a FunctionLiteral can be a subtype of a Callable type. Also an Instance type with a __call__ method. And a ClassLiteral type (based on its constructor signature.) And arguably SubclassOf as well, though this one is not sound since we won't enforce Liskov on constructor signatures.

Yes, that is correct. I'll open an issue to keep track of the remaining relationship, it might be something that a contributor could pick up.

Edit: #16953

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ty Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants