9月16日、Rust Glancerが「Why building a Rust LSP is hard · Rust Glancer」と題した記事を公開した。LSP(Language Server Protocol)とは、エディタとコード解析エンジンを繋ぐ標準プロトコルで、コード補完・ホバー表示・参照検索といった機能の基盤となる仕組みだ。Rustにおける代表実装がrust-analyzerだが、記事の著者は「Rust向けのLSPをゼロから実装してみる」という試みを通じて、LSP固有の難しさの構造を明らかにしている。
この記事が提示するテーマは明快だ。「LSP実装とは、不完全な情報から有用な答えを返し続ける作業である」。コンパイラはバイナリが生成できるかどうかの二値で動けばよい。しかしLSPは、ファイルが壊れていようが、インデックスが完成していまいが、ユーザーが入力するたびに何らかの答えを即座に返さなければならない。この非対称性こそが、LSP開発を独特に難しくしている。
起動直後から始まる困難:インデックスが完成する前にクエリが来る
LSPサーバーはクライアントからinitializeリクエストを受け取って起動する。しかしこの時点では、対象プロジェクトについて何も知らない。インデックスが完成するまで応答を遅らせればいいか? それだと10〜100秒近くエディタが無応答になりかねない。
rust-analyzerはこの問題を、設定のバリデーションのみ行ってすぐinitializeに応答し、ワークスペースの探索をレスポンス送信後にスケジュールすることで解決している。一方、著者が実験的に開発しているRust Glancerは、最初のクエリが届くまでパッシブに待機する設計を採用している。「まず動く」を優先するか「最初から完全を目指す」かという設計思想の差が、起動処理の時点から現れている。
クエリごとに要求される「精度」が違う
ハンドシェイクが終わると、すぐに最初のクエリが飛んでくる。代表的な4つを例に、必要な処理の重さを比べると以下のようになる。
- **
textDocument/documentSymbol**:ファイルをパースしてAST(抽象構文木)を作れば答えられる。最も軽い。 - **
textDocument/hover**:カーソル下のシンボルがどこから来ているかを解決するため、アイテムツリーの構築→モジュール解決→定義マップの生成というパイプラインが必要になる。 - **
textDocument/inlayHint**:関数の実装部分(ボディ)まで解析する必要があり、最終的には型推論が要求される。 - **
textDocument/references**:ワークスペース全体にまたがるすべての使用箇所を探す。Optionのような頻出名を追う場合、数千のボディを高速に走査しなければならない。テキストマッチングで候補ファイルを絞り込む「参照検索プラン」が必要で、rust-analyzerのこの記事が詳しい。
記事はここで重要な指摘をしている。
LSP構築とは病的なケース(Pathological Cases)をすべて潰す作業だ。
Optionの参照が多すぎるのは極端な例に見えるが、LSPの難しさはまさにこういったエッジケースの積み重ねで決まる。
rust-analyzerとRust Glancerのアーキテクチャの違い
インデックスの計算方法で、2つのLSPは対照的なアプローチを取っている。
rust-analyzerはsalsaを使う。salsaはRustエコシステム向けの「インクリメンタルコンピューテーションフレームワーク」で、入力と変換ロジックを定義すると出力が遅延評価・メモ化される。入力が変化した部分だけ再計算される仕組みで、「インデックスを一度作って終わり」ではなく、必要な状態をいつでも問い合わせれば計算してくれるデータベースのように機能する。ただし何も事前計算していないと初動がもたつくため、キャッシュプライミング(ワークスペース全体をセマンティック層まであらかじめ計算しておく処理)がデフォルトで有効化されている(該当PR)。
Rust Glancerは低メモリ消費・即時再起動を優先する。初回インデックスをファイルシステムにオフロードし、2回目以降は計算をほぼスキップする設計を目指している。ただし「インデックスをファイルに保存する」というアプローチ自体が独自の難しさを生む、と記事は述べている。どちらの設計にもトレードオフがあり、正解は一つではない。
VFSと競合状態の問題
LSPプロトコルはファイルシステムを抽象化しており、「ドキュメントと編集操作」だけが存在する世界を前提とする。しかし実際には、エディタ外でファイルが変更されることもあり、クライアントが変更通知を確実に送ってくれる保証もない。
これを解決するために両実装とも仮想ファイルシステム(VFS)を持ち、プロジェクトのソースをメモリ上で管理する。状態が変わるたびに「ソースジェネレーション(世代番号)」を更新し、古い状態に対して走っていた処理をキャンセルする仕組みだ。これがないと、ファイルシステムアクセスとLSP通知の混在が終わりのない競合状態を引き起こす。
「カーソル下にある何か」という概念の非自明さ
記事は最後に、「カーソル下にある何か」という概念自体が非自明であることを示唆して締めくくっている。LSPでよく使われるのはCST(Concrete Syntax Tree:コメントや空白まで含む完全な構文木)で、意味解析向けのASTとは異なる。このCSTがどのようにLSPクエリと組み合わされるかは続編記事で詳述されるとのことだ。
Rust Glancerは現在も開発中の実験的プロジェクトだが、rust-analyzerの内部構造を別の角度から照らすドキュメントとして、LSP開発に興味を持つ開発者には一読の価値がある。
詳細はWhy building a Rust LSP is hard · Rust Glancerを参照していただきたい。