13. grammar は 1 つだけ焼き込み、残りは読み込む
- Status: accepted
- Date: 2026-08-27
- Deciders: project owner
- 対象: v1.2.0 (本決定)。loader と置き場は v1.3.0
背景と問題
ADR-0012
がソースファイルを chunk にする方法を決めた。そのためには grammar が要る。そして
grammar は小さくない — 生成された parse table は言語ごとに巨大な C のソースで、
全言語を持ち歩くツールはその全部を持ち歩く。
groove の value proposition は「置いて実行するバイナリ 1 つ」だった。他に入れるものは無く、
ランタイムもパッケージマネージャも要らない。20 個の grammar を同梱する code parser は、
重さだけでその約束を壊す。かといって 1 つも同梱しなければ、素のままでは何も parse できない。
本決定が答える問い: バイナリはどの grammar を持ち、別の言語が欲しい利用者はどう入手するのか。
決定を左右した事情
- ダウンロードはダウンロードのままであること。散文しか index しない人が、一生 index しない
言語の代金を払うべきではない。
- 言語が欲しい人が、groove をソースからビルドせずに手に入れられること。
- 出荷物は既存の release パイプラインから再現できること。誰も作り直せない配布手段は腐る。
- grammar とそれ用に書かれた tags query は一緒に旅すること。grammar より新しい query は、
黙って何にもマッチしない。
- groove はネットワークに触らない。これは利用者が頼りにしている性質で、feature 1 つのために
使い切ってよいものではない。
検討した選択肢
- 全 grammar を焼き込む。 単純だが、その単純さの代金がサイズ。この道を採ったエディタでは
grammar が展開後サイズの大半を占めた。Markdown しか index しない利用者がそれを全額払う。
- cargo feature で、利用者が欲しい組み合わせをコンパイルする。 ソースからビルドする人に
しか効かない。release パイプラインが作るのは platform ごとに 1 つの artifact で、
feature の組み合わせ行列は作れない。結局、公開バイナリはどれか 1 組を選ぶことになる —
それは選択肢 1 か 5 が名前を変えたものでしかない。
- 1 つだけ焼き込み、残りは groove 自身が配る動的ライブラリにする (採用)。
- WebAssembly plugin。 サンドボックスされるのが魅力。だが組み込みランタイムだけで
数 MB かかり、plugin を一度も読まない人まで払う — 選択肢 1 を却下した理由そのもの。
toolchain の下限も上がり、macOS では JIT に署名済みアプリが要求すべき entitlement が要る。
- 何も焼き込まず、全言語を plugin にする。 選択肢 3 の feature を off にしただけの
同じコード経路なので、選べる状態は保たれる。ただし既定にすると、利用者が何かを
取りに行くまで 1 行も parse できない。
- 初回に言語バンドルを DL する。 便利だが、便利さのためにネットワークの性質を使い切る。
入手可能なバンドルは platform ごとに大きく、query を含んでいない。
決定
選択肢 3 を採る。Rust は既定で on の feature の後ろに焼き込む。他の言語はすべて、
groove 自身の release パイプラインが公開する別の動的ライブラリとし、利用者が groove の
読む置き場に配置する。
最初の 1 言語が Rust である理由は 2 つ。groove 自身がそれで書かれているので、groove を
自分のリポジトリに向けるだけで追加物なしに動く。そして、その grammar はバイナリ増分
1 MiB 強で測れた — 全員に渡せる大きさである。
tree-sitter 本家が公開しているのはソース書庫と WebAssembly ビルドであって native ライブラリ
ではないので、ライブラリは groove が作り、groove が責任を持つ。バイナリと同じ release job が、
grammar に直接依存する crate から生成する — これが grammar と tags query を「一緒にビルドされた
版」に保つ。
結果と代償
- plugin は native code であり、読み込むこと自体が実行である。 ライブラリ自身の初期化は
groove が symbol を 1 つ検査するより前に走るので、壊れた / 悪意あるファイルは groove
プロセスにできることを何でもできるし、その後 groove が行う検査はそれを変えない。
ここから 2 つが従う: groove は設定が実際に要求した言語のファイルだけを開き、置き場を
丸ごと開くことはしない。そして配置手順は、展開前に公開チェックサムを照合するよう指示する。
groove の release 以外から持ってきたライブラリを置くことは、この設計が守る範囲の外にある。
- plugin の置き場は特権的な設定になる。 知識ベースの隣で見つかった設定ファイルは
そこを指す権限を持たない。モデルキャッシュの差し替えを許さないのと同じ理由で、
「このフォルダを index して」が「このコードを実行して」に化けるからだ。untrusted な設定を
検出したら、そのファイルがキーを書いていたかどうかに関わらず安全な既定へ差し戻す —
キーを省くことが規則の抜け道になってはならない。(v1.3.0 以降、同じことが 1 段上流の
[parsers].enabled にも適用される: 言語を名指しすることこそがライブラリを開かせる行為なので、
置き場だけを守っても点火装置が無防備なままだった。このキーは省略時の差し替えが要らない —
[parsers] を書かなければ既に Markdown のみだから。)
- 有効化された言語が解決できなければ実行を止める。 何が足りず、どこへ置くのかを述べ、
データベースは作らない。これは設定が trusted かどうかに関わらず成り立つ。そうしないと、
同じ物理的状況が「ファイルがどこで見つかったか」で 2 通りの意味を持ってしまう。
- 言語の追加には groove の release が要る。 groove は言語 id からライブラリのファイル名への
対応表を持ち、何かを開く前にその名前を知っている必要があるからだ。任意のライブラリを
置き場に放り込んでも何も起きない。
- release のファイル数は 1 言語につき 8 増える (対応 platform ごとに書庫とチェックサム)。
参考
grammar の契約は crates/groove-grammar-abi にある。本決定が支える chunk 化の決定は
ADR-0012。
本決定は、その機構より先に出荷される。 v1.2.0 が運ぶのは焼き込まれた Rust grammar と
共有の契約まで。loader・置き場の設定・公開されるライブラリは v1.3.0 で届く。v1.2.0 を読んで
plugin を置こうとした人は、まだ置き場を見つけられない。