Ruffに搭載される新しい型チェッカーRed-knotについて
Python開発者向けツールRuff/uvの開発チーム(Astral inc.)がPython向けの型チェッカーを新規開発しているので紹介いたします。この型チェッカーはコードネーム"Red-knot"と呼ばれており、まだリリース前ですが将来的にはRuffに組み込まれる予定であるようです。以下ではこのプロジェクトの概略を説明したいと思います。
特徴
既存の型チェッカー(e.g. mypy, pyright)の不満点である遅さを改善すべく、徹底的にパフォーマンスに気を配った設計となっています。pyrightもそこまで遅くはないのですが、さらに高速であることを目指すようです。
Red-knotはRuffと同様Rustを用いて実装されています。Ruffが使っている諸々のデータ構造を共有しているため、フォーマット、lint、静的解析が一気通貫で行えるようになります。JavaScriptツールチェインのBiome(旧Rome)を彷彿とさせる遠大な計画ですが、Ruffとuvの実績から考えてもこれが夢物語ではないことがわかるでしょう。
mypyやpyrightと違ってシングルバイナリでリリースされるので、CI上でのセットアップが簡単といったメリットもあると思います。
またRed-knotはsalsaというrust-analyzerでも使われているオンデマンド計算のためのフレームワークも採用しています。コンパイラ、静的解析ツールのような繰り返し呼ばれるCPUバウンドなツールでは、しなくていい(もうした)計算をどれだけショートカットするかがパフォーマンスの支配要因となる場合が多いです。salsaはこれを実現してくれるフレームワークであり、ソースの変更に対して再検査すべき最小限の部分を特定してそこだけ再計算を行うようになっています。
実際のライブラリから抽出したスクリプトによるベンチマークも行われており、現実のコードベースに対するパフォーマンスの検証にも余念がありません。
また、ただ単に既存の型チェッカーの実装に追従するだけでなく、独自の機能も実装しています。代表的なものがIntersection type (&
)で、これはある二つの型両方の性質を持つ型です。
def _(x: int | None, y):
if x is not None:
x # (int | None) & ~None = int
if y is not None:
y # Unknown & ~None
if x:
x # int & ~AlwaysFalsy
~T
は「T
以外」を表す型で、AlwaysFalsy
は__bool__()
が常にFalse
を返すオブジェクトの型で、これもred-knot独自の拡張型です。
これらはPythonのtypingに正式に導入されている機能ではないのですが、型チェッカー内部でこのような型を持っているとnarrowing(条件分岐による型の絞り込み)などの文脈で非常に便利であるため導入されています。
これらのAPIはred-knotが認識するknot_extensions
という仮想的なモジュールから明示的に使用することもできます。
ただしこれらのAPIが実体のあるモジュールとして提供されるかは未定です。
# Sample from https://github.com/mtshiba/ruff/blob/db0de69a982958ba32b0c7de266c8862df1a32f7/crates/red_knot_python_semantic/resources/mdtest/type_api.md#intersection
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
from typing_extensions import Literal, Never
class S: ...
class T: ...
def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None:
reveal_type(x1) # revealed: S & T
reveal_type(x2) # revealed: S & ~T
def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None:
reveal_type(y1) # revealed: int
reveal_type(y2) # revealed: bool
reveal_type(y3) # revealed: Never
def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None:
reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2]
class A: ...
class B: ...
class C: ...
type ABC = Intersection[A, B, C]
static_assert(is_subtype_of(ABC, A))
static_assert(is_subtype_of(ABC, B))
static_assert(is_subtype_of(ABC, C))
class D: ...
static_assert(not is_subtype_of(ABC, D))
現在の実装状況
Red-knotは数年内(うまくいけば2025年内にリリースできるかも?)に最初のリリースを行う予定で開発が進んでいます。
既に基盤となる部分はかなり整備されており、
- 主要な構文の意味解析
- シンボル解決
- 型注釈の検査
- 基本的な型推論
- control flow based type analysis
ぐらいまでは実装済みです。既に高いコンパイル時計算能力を有しているので、mypyよりpyright寄りの積極的に型推論を行う型チェッカーになりそうです。
互換性に関しては、なるべく既存の型チェッカーとの互換性を保つように設計されており、両者の挙動が異なる場合は「より妥当」な挙動を採用するという方針となっているようです。従ってmypyやpyrightからの移行コストはそこまで高くないと思いますが、一応開発陣からはRed-knotを「mypyやpyrightのdrop-in replacementにするつもりはない」との見解が出ています。mypyやpyrightでも細部であまり合理的でない挙動をすることがあるので、これは悪くない判断だと思います。
ジェネリクスやオーバーロードなどがまだ実装されていないため、コレクションや四則演算など基本的な部分が動かないというところもあるのですが、拙速に機能を足していくのではなく初めから最良の設計で型チェッカーを構築しようという意思が感じられます。
Red-knotが完成すればAstralの目玉ツールになることは間違いないでしょう。
試してみるには
今のところバイナリ配布などはないので、自前でビルドする必要があります。
Rustで書かれているので、Rustツールチェインのインストールが必要です。
git clone https://github.com/astral-sh/ruff.git
cd ruff
cargo install --path crates/red_knot
以下のコマンドでチェックができます。
red_knot check
# A specific file
red_knot check foo.py
pyproject.toml
にも対応しているようです。
[tool.knot]
# Environment settings
environment.python-version = "3.11" # Target Python version
environment.python = ".venv" # Path to Python environment (for third-party imports)
environment.extra-paths = ["path/to/extra/modules"] # Additional module search paths
# Terminal settings
terminal.output-format = "full" # "full" or "concise"
terminal.error-on-warning = true # Exit with error code on warnings
# Configure rule severities
rules.call-non-callable = "error"
rules.call-possibly-unbound-method = "warn"
まだ機能は限られていますが、red_knot server
でlanguage serverとしても起動できるようです。
余談
なんで私が開発段階であるRed-knotについてそれなりに知っているかと言うと、筆者が開発チームに参加しているからです。詳しい経緯についてはこちらの記事をご覧ください。
Discussion