世界を動かす技術を、日本語で。

ツリーシッターと言語サーバー

概要

Tree-sitter は高速でエラー耐性のあるパーサ生成器であり、主に構文ハイライトに利用 Language Server はコード解析とインテリジェントな支援をエディタに提供 両者は役割が異なり、Tree-sitterは構文解析、Language Serverは意味解析が得意 LSP(Language Server Protocol)によりエディタとサーバ間の連携が標準化 Rustなど一部言語ではLanguage Serverによる高度なハイライトも可能

Tree-sitterとは何か

  • Tree-sitterパーサ生成器
  • プログラミング言語の 文法記述 を入力し、 構文解析プログラム を自動生成
  • 高速動作構文エラー耐性 が特徴
  • テキストエディタでの 構文ハイライト 用途に最適
    • 入力途中の 不完全なコード でもハイライトが崩れにくい
    • 従来の 正規表現ベース のハイライトはエラー時に壊れやすい
  • クエリ言語 を使い、構文木に対して 特定要素の検索 が可能
    • EmacsのCitar拡張などで Typst サポートに活用
  • 言語実装 に忠実なハイライト実現
    • 単なるパターンマッチではなく 言語エンジン同等の解析 が可能

Language Serverとは何か

  • Language Serverプログラム解析サービス
  • エディタに 記号の定義位置補完候補 などを提供
  • Language Server Protocol(LSP) でエディタとサーバが JSONメッセージ で通信
    • オープン標準 であり、任意の言語・エディタで利用可能
  • N×M問題 の解消
    • 以前は言語数N×エディタ数M分の個別実装が必要だった
    • LSPにより 各言語は1つのLanguage Server各エディタはLSP対応 で済む
  • ランタイムやコンパイラとの連携 により 意味的に正確な解析 が可能
    • 例:同名関数popが複数ある場合、 どのモジュールのものか正確に特定
  • dumb-jump のような単純ツールは スコープ判定が弱い が、Language Serverは強力

構文ハイライトにおけるLanguage Serverの活用

  • Language Server でも 構文ハイライト は可能
    • ただし、 Tree-sitterより複雑かつ重い 場合が多い
  • Emacsの eglot-semantic-tokens-mode でLSPベースのハイライト対応
    • Rustコードでの使用例あり
    • Tree-sitterベースのハイライト で十分なため、特別な理由がなければそちらを利用
  • Rust-analyzer など一部Language Serverは 変数のmutability (可変・不変)を区別してハイライト可能
    • 通常の構文ハイライトでは不可能な 詳細情報の可視化 が可能

まとめと所感

  • 本記事は Ashton Wiersdorf による 完全な人間執筆
  • LLM(大規模言語モデル)は 翻訳や単純作業の自動化 には有用
  • 複雑な部分は 自分で書いた方が速い 場合も多い
  • LLM生成文も役立つが、 本記事は人間の思考と意図を込めて執筆
  • 本物の意味を持つ文章 の価値と、それを作る楽しさ

Hackerたちの意見

: https://github.com/tree-sitter-grammars/tree-sitter-yaml

変だな、yaml-ts-modeは存在するのに?パーサーの取得方法が変わったのかな?

うーん… 特定の言語にArchlinuxのパッケージがないからって、その言語にtree-sitterのサポートがないわけじゃないよね?例えば、https://github.com/Goldziher/tree-sitter-language-pack にある長い言語リストを見てみて。RやYAML、Golang、他にもたくさん含まれてるよ。

tree-sitter-yamlは確実に存在するよ[1]。おそらく、まだArch用にパッケージ化されてないだけだね。それは貢献できることかも。

私のemacs設定には、以下のパーサーがインストールされてるよ:awk、bash、bibtex、blueprint、c、c-sharp、clojure、cmake、commonlisp、cpp、css、dart、dockerfile、elixir、glsl、gleam、go、gomod、heex、html、janet、java、javascript、json、julia、kotlin、latex、lua、magik、make、markdown、nix、nu、org、perl、proto、python、r、ruby、rust、scala、sql、surface、toml、tsx、typescript、typst、verilog、vhdl、vue、wast、wat、wgsl、yaml。

Goはここにあるよ: https://github.com/tree-sitter/tree-sitter-go Googleで探してみて。他のも多分どこかにあるはず。

ほとんどは言語パックに入ってるよね(https://github.com/Goldziher/tree-sitter-language-pack)。他のはちょっと最適じゃない答えだけど、最新のLLMを使って文法を生成してみたら、意外と良い感じにできたよ(数回の試行で)。とはいえ、構文ハイライトや製品に組み込む以上のことをするなら、もっと時間をかけた方がいいと思う。

https://tree-sitter.github.io/tree-sitter/#parsers https://github.com/tree-sitter/tree-sitter/wiki/List-of-pars...

これはオレンジとジュースの違いみたいなもんだね。オレンジを絞ってジュースを取り出すこともできるけど、それだけがオレンジの使い方じゃないし、ジュースを作る方法もそれだけじゃない。私はtree-sitterを使ってカスタムプログラミング言語を開発してるけど、CSTからASTにするためにはもう一手間かかる。でも全体的な開発体験は、手動でパーサーを作るよりずっと早いよ。

毎回Treesitterの素晴らしさを褒めるチャンスがあるときは、必ずその機会を利用してるよ。ほんと大好きなんだ。いろんなパーサージェネレーターを試してみたけど、TSのアプローチはシンプルでめちゃくちゃ良いから、もう他のは使わないと思う。反復速度のおかげで、まるで禅の境地に入ったみたいに、文法設計だけに集中できて、技術的な部分は気にしなくて済むんだ。

N00bな質問だけど、言語パーサーは「com.foo.bar.Bazはここで定義されている」みたいな具体的な情報をくれるよね。Tree-sitterもそういうことしてくれるの?それとも「このファイルにはBazのシンボル宣言がある」とか、「このファイルの別のところには‘com.foo.bar’のパッケージ宣言がある」って言って、結局自分で調べなきゃいけないの?

CSTからASTに移行するための追加ステップ これについて詳しく教えてもらえる?新しい言語のパーサーとしてtree-sitterを使うことを考えてるんだけど、複数の構文をサポートするかもしれない。パースツリーを共通のスキーマに変換するつもりなんだけど、それがターゲット言語になると思う。具体的な構文木と抽象構文木の違いがいまいち分からないんだけど、前者は言語の意味に関係ない情報、例えば空白とかを含むってこと?

うん、tree-sitterを使って言語サーバーを実装することもできるよ。実際、仕事で使ってるカスタムスクリプト言語でやったことがある。

余談だけど、記事を書くのにAIを使わないっていう注意喚起、ありがとう。ネットで情報を探して、答えがありそうな記事を見つけても、著者の信頼性が不明で疲れちゃうんだよね(これ、Mediumではよくあること)。

そうだね、これがなぜなのか考えてたんだ。書くことが私たちに考えさせるからだと思う。何かを書くとき、考えが十分にまとまっていないことが多いし、言葉にすることで自分の思考の論理的な失敗に気づけるんだ。それを解決する能力も得られる。だから、AIに書かせるだけだと、考える部分を避けてしまって、書かれた記事は読者にとってあまり役に立たなくなる。声を使って記事にするのは少しマシだけど、やっぱり書く方が効果的だと思う。AIの文章を見分けるのは簡単だけど、話しながら考えた人と、あまり考えずにAIに書かせた人の違いを見分けるのは難しい。だから、AIの文章の匂いを嗅ぎ取ると、思考の蒸留が少ないという合理的な期待が生まれるんだ。

Tree-sitterは最高だね。EmacsのCombobulateを支えてる。構造化された編集や移動は、これがなかったら簡単にはできなかったよ。

やあ、ミッキー!Emacsのスペースで作ったものに感謝してるよ。ここでコメントしてくれてありがとう。:)

構文ハイライトに言語サーバーを使うことは可能。これをやりたい(またはやりたくない)特に強い理由は知らないな。うーん、強い理由はレイテンシーとレイアウトの安定性かも。Tree-sitterはメインスレッド(または近いワーカー)で通常はミリ秒未満の時間でパースするから、構文の色付けがキーストロークと同期してるんだ。LSPのセマンティックトークンは設計上非同期だし。ハイライトにLSPだけに頼ると、毎回タイプするたびにスタイルのないコンテンツのフラッシュや色のシフトが起きるから、サーバーへの往復(ローカルでも)やその後の再トークン化がフレームの予算よりも時間がかかるんだ。理想的なハイジーンは、tree-sitterが高速な字句の色付け(キーワード、句読点、基本的な構造)を瞬時に提供して、LSPがセマンティック修飾子(インターフェースとクラス、ミュータブルと定数)を200ms後に非同期で描画するような感じかな。LSPに基盤を頼ると、エディタがもっさりした感じになる。

大体、両方をサポートしているエディタではこんな感じで動くよね。Tree-sitterはまあまあエラー修正ができて、速度(君が言った通り)や柔軟なクエリ言語のおかげで、動作するパーサーを素早く試すには最適だし、実際のエディタに統合するのも明らかに簡単だよ。あ、いくつかのLSPはTree-sitterを使ってパースしてるよ。

うーん、強い理由はレイテンシとレイアウトの安定性かもしれないね。Tree-sitterは通常、メインスレッド(または近いワーカー)でサブミリ秒の時間枠で解析するんだ。ここで「Roslyn」の設計者/アーキテクトの一人なんだけど、これはC#/VBコンパイラ、VS IDE体験、そして私たちのLSPサーバーを支えるセマンティック分析エンジンなんだ。注:Roslynでは、マイクロ秒(ミリ秒ではなく)での解析を目指してる。非常に大きなファイルでも、初期解析がミリ秒かかっても、99.99%以上の編集がマイクロ秒で行われるインクリメンタルパーサーデザイン(https://github.com/dotnet/roslyn/blob/main/docs/compilers/De...)を持っていて、99.99%以上の構文ノードを再利用し、独立した不変のツリーを生成する(これにより、これらのツリーを同時に消費するスレッド間での共有に関する問題がない)。 > 文字を入力するたびに、スタイルのないコンテンツや色が変わるアーティファクトが一瞬現れるのは、サーバーへの往復(ローカルでも)とその後の再トークン化にかかる時間がフレームの予算を超えているからだ。これはどこかに深刻な問題があることを示してる。現代のUIスタックと何も変わらないよ。現代のUIスタックは、ブロックする可能性のある外部コードが入ってくることを望まない。だから、すべての、潜在的に無制限な処理作業はUIスレッドの外で行われて、常にそのスレッドが応答するようにするんだ。「サーバーへの往復(ローカルでも)」は、処理スレッドへの往復と何も変わらない。実際、Visual Studioでは、サーバーを別のプロセス空間で実行する必要がないから、そうやって動いてる。代わりに、RoslynのLSPサーバーはVS内で通常のライブラリとしてプロセス内で実行される。他のコンポーネントが以前にこの作業をしていたのと何も変わらない。 > LSPに基づくレイヤーに頼ると、エディタがもっさりした感じになる。そんなことはないはずだよ。注:これにはある程度の賢い作業が必要だ。例えば、Roslynの分類システムでは、分類スレッドのカスケードセットがある。一つは字句的に分類するもの、もう一つは構文、次はセマンティクス、最後に埋め込み言語(埋め込まれた正規表現やJSON、あるいはC#の中にネストされたC#を想像してみて)用のものだ。そして、もちろん、これらの埋め込み言語にもカスケード分類があるよ :D この概念はLSPの他の場所でも使われていることに注意してね。例えば、私たちの診断サーバーは、コンパイラの構文、コンパイラのセマンティクス、サードパーティのアナライザーを別々に計算するんだ。このアプローチにはいくつかの利点がある。まず、マシンの能力に応じてスケールアップできること。だから、空いているコアがあれば、あまり重要でないデータを同時に計算するために使える。次に、ある操作で結果が計算されると、それをユーザーに表示できる。残りが終わるのを待つ必要はない。細かく分けることで、UIがシャープで応答性があるように見え、時間がかかる操作は遅くなるけど最終的には表示される。例えば、コンパイラの構文診断は一般的にマイクロ秒かかるけど、サードパーティのアナライザー診断は数秒かかることもある。後者が実行されるのを待っている間に前者を止める意味はないよ。LSPはこういうことを簡単にマルチプレクスできるんだ。

(こんにちは、私はrust-analyzerチームの一員だけど、プロフィールに書いてある理由であまり活動してないんだ。)> 言語サーバーは、言語のランタイムやコンパイラツールチェーンにフックして、ユーザーのクエリに対して意味的に正しい答えを得られるから強力なんだ。例えば、スタックライブラリからインポートしたポップ関数と、ヒープライブラリからのポップ関数の2つのバージョンがあるとする。Emacsのdumb-jumpパッケージみたいなツールを使ってポップの呼び出しの定義にジャンプしようとすると、どのモジュールがスコープにあるのか分からなくて混乱するかもしれない。一方、言語サーバーはこの情報にアクセスできるから、混乱しないはずだよ。言語サーバーは一般的に正しいナビゲーションやオートコンプリートを提供するけど、必ずしも既存のコンパイラにフックする必要はない。言語サーバーは既存のコンパイラツールチェーンのレイテンシーに敏感な再実装かもしれないし(rust-analyzerが一番馴染みがあるけど、最近の新しい言語サーバーは、言語のコンパイラがクエリ指向でない場合、この方向に進むことが多い)。> 言語サーバーを使って構文ハイライトをすることも可能だよ。これをやりたい(またはやりたくない)特に強い理由は知らないな。私はRustを書くのに多くの時間を使うから、Rustを例に挙げると、ミュータブルなバインディングをハイライトしたり、enumやstructを違うスタイルにしたりできる。これは慣れると大きな影響を与える小さなことの一つだね。意味的な構文ハイライト(LSP仕様で呼ばれているもの)がないエディタは、私には裸に感じる。

ミュータブルなバインディングをハイライトしたり、enumやstructを違うスタイルにしたりできる。わお!それはすごく良い理由だね。知らなかったことを教えてくれてありがとう。:) 追記:rust-analyzerの能力についての段落を追加したよ。再度ありがとう!

Rustの意味を意識したハイライトの別の例として、Flowistryを見てみて。これを使うと、式を選択して、その式に影響を与えるコードや影響を受けるコードをすべてハイライトできるよ。: https://github.com/willcrichton/flowistry

TypeScriptみたいな言語がコンパイル時間を改善するために別のプログラミング言語を使ってるのが面白いと思う。逆にRustみたいな言語は、もう最速の言語を使ってるのに、コンパイルはまだ遅いから、rust-analyzerみたいな解決策に頼らざるを得ないんだよね。

言語サーバーを使って構文ハイライトをすることも可能だよ。これをやりたい(またはやりたくない)特に強い理由は知らないな。言語サーバーはもっと複雑なプログラムになり得るし、構文に関する特に詳細な情報を提供できるかもしれない。Tree-sitterより遅いかもしれないしね。私たち(TypeScript)は、tmLanguageの前にVisual Studioでこれをやってたんだ。2つ目のパーサーを書く必要がなかったから良かったよ。私たちのパーサーはすでにエラー耐性があってインクリメンタルだったし、構文ハイライトは構文木のトークンに降りていくだけだったから。だから、パーサーのバグが発生する余地はなかったし、tmLanguageのような限られたフォーマットで奇妙さや曖昧さを解決するロジックをどうやってエンコードするかを考える必要もなかった。これらはすべてTSServerの前の話で(TSServerはLSPの前だけど、TypeScript 7で来る予定)。JSONを介した構文ハイライトのレイテンシーは大きすぎて、他のエディタはtmLanguageの外で構文ハイライトを提供しないことが多かった。結局、意味的なハイライトが登場して、これはレイテンシーに対してより耐性があり、VS Codeの構文ハイライターの上に色を重ねる形になった。このアプローチのもう一つの問題は、速い構文ハイライトのために専用のスレッドが必要だったことだ。そのスレッドはJS言語サービスの別のインスタンスで、何も共有していなかったから、構文ハイライトのためだけにかなりのメモリオーバーヘッドがあったんだ。

面白いタイミングだね。仕事で使ってるニッチなDSLのためにLSPを作ってるんだけど、昨日ふと思ったんだ。自分のLSPがやってる構文ハイライトって、全部TSクエリとプロトコル用に正しくエンコードしたものなんだよね。それで、LSPのフックアップを提供するvscodeの拡張機能でそれができるか調べてたところ。拡張とLSPで同じtree-sitterの文法が使えるのはちょっといいよね、言語は違うけど。

Tree Sitter用のLSPは構文ハイライトには不十分だと思うけど、Emacsでの自分の特別な組み合わせにはめっちゃ満足してる。特別なパターンのためにTree Sitterクエリを書くのがすごく簡単なのが好きなんだ。例えば、名前空間の宣言をスコープ解決とは違う風にハイライトしたり、インラインアセンブリを普通の文字列とは違う風にハイライトしたりね。でも、言語サーバーからのセマンティックハイライトが本当に欲しい。定数やマクロを特別にハイライトするようなやつ。Emacs(他のエディタもだけど)だと、両方の強みを簡単に組み合わせられるんだよね。

ちょっと付け加えたいんだけど、treesitterはヒューリスティックなインクリメンタルパーサーなんだ。普通のパーサーとtreesitterの違いは、普通のパーサーはファイルの先頭からトークンを読み始めて、そこからASTを組み立てようとするのに対して、treesitterは任意のポイントからトークンを取得してASTノードに組み立てていくんだ。そして、ファイル全体が解析されるまでASTを拡張しようとする。この方法はインクリメンタルな編集をサポートするけど(修正した部分のASTを捨てて再解析できるから)、問題はほとんどの言語が左から右に解析されるときに曖昧さがないように設計されていること。こういう解析だと、リトライや推測が必要になることがある。あと、Goみたいな現代の言語はセマンティック分析なしで解析できるように設計されてるけど、C/C++みたいな古い言語はそうじゃないから、シンボルテーブルが必要なんだ。この場合、treesitterは推測しなきゃいけなくて、間違った推測をすることもある。ASTで何ができて何ができないかというと、関数呼び出しか変数参照か、他の構文の一部かはわかるけど、例えばx = 2;って書いたら、treesitterはxが何か分からない。floatなのかintなのか、ローカルなのかクラス変数なのかグローバルなのか。これを判断するにはコンパイラがシンボルを逆参照するために使うシンボルテーブルが必要だけど、treesitterはそれをやってくれないんだ。