ある言語で記述されたテキストをパースするために、tree-sitterはその言語のグラマー(grammar:
文法)に依存します。Emacsにおける言語グラマーはシンボルによって表現されます。たとえばC言語のグラマーはシンボルc
として表現されます。このc
というシンボルはtree-sitter関数のlanguage引数として渡すことができます。
tree-sitterの言語グラマーはダイナミックライブラリーとして配布されています。ある言語のグラマーをEmacsで使用するためには、そのダイナミックライブラリーがシステム上にインストール済みかを確認する必要があります。Emacsは以下の順序で複数の場所から言語グラマーを探します:
treesit-extra-load-path
で指定されたディレクトリーのリストから;
user-emacs-directory
で指定されるディレクトリーのサブディレクトリーtree-sitterから(initファイルを参照);
これらのディレクトリーそれぞれにおいて、Emacsは変数dynamic-library-suffixes
が指定するファイル名拡張子をもつファイルを探すのです。
Emacsがライブラリーを見つけられなかったりロードに問題がある場合には、Emacsがtreesit-load-language-error
エラーをシグナルします。このシグナルのデータは以下のいずれかです:
(not-found error-msg …)
その言語のグラマーライブラリーをEmacsが見つけられなかったという意味。
(symbol-error error-msg)
すべての言語のグラマーライブラリーでエクスポートされているべき関数を、そのライブラリーではEmacsが見つけられなかったという意味。
(version-mismatch error-msg)
その言語のグラマーライブラリーとtree-sitterライブラリーのバージョンに互換性がないという意味。
上記すべてのケースにおいて、error-msgにより失敗に関する追加の詳細が提供されるかもしれません。
この関数はlanguageにたいする言語グラマーが存在して、それがロード可能であれば非nil
をリターンする。
detailが非nil
の場合には、languageが利用可能なら(t
. nil)
、利用不可なら(nil
. data)
をリターンする。dataはtreesit-load-language-error
のシグナルデータ。
慣例によりlanguage用ダイナミックライブラリーのファイル名はlibtree-sitter-language.extです。ここでextはダイナミックライブラリー用のシステム固有な拡張子です。同じく慣例により、そのライブラリーが提供する関数の名前はtree_sitter_language
です。この慣例にしたがっていない言語グラマーライブラリーの場合には、
(language library-base-name function-name)
上記エントリーを変数treesit-load-name-override-list
のリストに追加する必要があります。ここでlibrary-base-nameはダイナミックライブラリーのファイル名のベースネーム(basename:
先行するディレクトリー部分を除外したファイル名のことで、通常だとlibtree-sitter-language)、function-nameはそのライブラリーが提供する関数(通常だとtree_sitter_language
)です。たとえば、
(cool-lang "libtree-sitter-coool" "tree_sitter_cooool")
これは慣例に屈するには自分があまりにも“cool”に過ぎると考える言語の例です。
この関数はtree-sitterライブラリーがサポートしている言語グラマーのABI (Application Binary
Interface:
アプリケーションバイナリーインターフェイス)のバージョンをリターンする。デフォルトではそのライブラリーがサポートする最新のABIバージョンをリターンするが、min-compatibleが非nil
の場合にはそのライブラリーでまだサポートできる最古のABIバージョンをリターンする。言語グラマーライブラリーはtree-sitterライブラリーがサポートする最古と最新の間にあるABIバージョンにたいしてビルドしなければ、tree-sitterライブラリーがそれらをロードできなくなる。
この関数はEmacsがロードしたlanguage用の言語グラマーライブラリーのABIバージョンをリターンする。languageが利用できなければnil
をリターンする。
構文ツリーはパーサーによって生成されます。構文ツリーにおけるノードはそれぞれがテキストのある部分を表し、お互いが親子関係というリレーションシップによって接続されています。たとえば以下のようなソーステキストがあるとします
1 + 2
これは以下のような構文ツリーになるかもしれません
+--------------+ | root "1 + 2" | +--------------+ | +--------------------------------+ | expression "1 + 2" | +--------------------------------+ | | | +------------+ +--------------+ +------------+ | number "1" | | operator "+" | | number "2" | +------------+ +--------------+ +------------+
これを以下のようにS式で表すことも可能です:
(root (expression (number) (operator) (number)))
root
、expression
、number
、operator
のような名前はノードのタイプ(type:
型)を指定します。ただし構文ツリーのすべてのノードがタイプをもつ訳ではありません。タイプをもっていないノードは無名ノード(anonymous
nodes)、タイプをもつノードは名前つきノード(named
nodes)と呼ばれています。無名ノードは角カッコ‘]’のような区切り文字やreturn
のようなキーワードを含む、固定化された綴りのトークン(token:
字句単位)です。
構文ツリーの分析を容易にするために、多くの言語グラマーは子ノードにフィールド名(field
names)を割り当てています。たとえばfunction_definition
ノードはdeclarator
とbody
のフィールド名をもつかもしれません:
(function_definition declarator: (declaration) body: (compound_statement))
言語の構文の理解、および構文ツリー割り当て使用するLispプログラムのデバッグ支援のために、Emacsはカレントバッファーのソースの構文ツリーをリアルタイムで表示する“explore”モードを提供しています。更にEmacsにはポイント位置にあるノードの情報をモードラインに表示する“inspect”モードも付属しています。
このモードはカレントバッファーのソースの構文ツリーを表示するウィンドウをポップアップする。ソースバッファーでテキストを選択することによって、表示されている構文ツリーの対応する部分がハイライトされる。構文ツリーでノードをクリックすれば、ソースバッファーの対応するテキストがハイライトされる。
このマイナーモードはポイント位置で始まるノードをモードラインに表示する。たとえばモードラインに以下のように表示されるかもしれない
parent field: (node (child (...)))
ここでnode、child、...等はポイント位置で始まるノード、parentはnodeの親である。nodeはbold書体で表示される。field-nameはnode、child、...等のフィールド名である。
ポイント位置で始まるノードがない(ポイントがノードの中間にある)場合には、ポイントを跨ぐ(span)もっとも前のノード、およびそのノードの直近の親ノードがモードラインに表示される。
このマイナーモード自身はパーサーを作成せず、(treesit-parser-list)
の最初のパーサーを使用する(Tree-sitterパーサーの使用を参照)。
言語グラマーの製作者はプログラミング言語のグラマー(grammar: 文法)を定義します。パーサーがどのようにしてプログラムテキストから具体的な構文ツリーを構築するかを決めるのがグラマーです。構文ツリーを効果的に使用するためには、グラマーファイル(grammar file)を調べる必要があります。
グラマーファイルは通常だと言語グラマーのプロジェクトレポジトリにあるgrammar.jsです。言語グラマーのホームページへのリンクはtree-sitter’s homepageで見つけることができるでしょう。
グラマー定義はJavaScriptによって記述されます。たとえばfunction_definition
ノードにマッチするようなルールは以下のようなものかもしれません
function_definition: $ => seq( $.declaration_specifiers, field('declarator', $.declaration), field('body', $.compound_statement) )
ルールは単一の引数$を受け取る関数によって表現されます。この関数がグラマー全体を表すのです。この関数自体は他の関数によって構築されています。一連の子ノードをまとめるのがseq
関数、子ノードにフィールド名の注釈をつけるのがfield
関数です。上記の定義を俗にBNF
(Backus-Naur Form: バッカス・ナウア記法)と呼ばれる構文で表せば以下のようになるでしょう
function_definition := <declaration_specifiers> <declaration> <compound_statement>
そしてパーサーがリターンするノードは以下のようになります
(function_definition (declaration_specifier) declarator: (declaration) body: (compound_statement))
以下はグラマー定義で目にするかもしれない関数のリストです。これらの関数はいずれも引数として他のルールを受け取り新たなルールをリターンします。
seq(rule1, rule2, …)
すべてのルールに逐一マッチする。
choice(rule1, rule2, …)
引数のルールいずれかにマッチする。
repeat(rule)
ruleに0回以上マッチする。正規表現の演算子‘*’に似ている。
repeat1(rule)
ruleに1回以上マッチする。正規表現の演算子‘+’に似ている。
optional(rule)
ruleに0回または1回マッチする。正規表現の演算子‘?’に似ている。
field(name, rule)
ruleにマッチする子ノードにフィールド名nameを割り当てる。
alias(rule, alias)
ruleにマッチしたノードをパーサーが生成する構文ツリーでaliasとして表示する。たとえば、
alias(preprocessor_call_exp, call_expression)
これによりpreprocessor_call_exp
がマッチしたノードがcall_expression
と表示される。
以下は言語グラマーを読むにあたってそれほど重要ではないグラマー関数です。
token(rule)
単一の葉ノード(leaf node)としてruleをマークする。つまり個別の子ノードをもつ親ノードではなく、その単一の葉ノードにすべてが収斂されるようなノードを生成する。ノードの取得を参照のこと。
token.immediate(rule)
通常のグラマールールは先行する空白を無視するが、これは空白が前置されていないruleだけにマッチするよう変更する。
prec(n, rule)
ruleにたいしてレベルnの優先度を与える。
prec.left([n,] rule)
ruleにたいしてオプションとしてレベルnを付与して左結合(left-associative)とマークする。
prec.right([n,] rule)
ruleにたいしてオプションとしてレベルnを付与して右結合(right-associative)とマークする。
prec.dynamic(n, rule)
prec
と似ているが優先度は実行時に適用される。
tree-sitterプロジェクトにはmore about writing a grammarというドキュメントがあります。特に“The Grammar DSL”というセクションを読んでください。