Tree-sitterは、その言語のテキストを解析するために言語文法を利用します。Emacsでは、言語文法はシンボルで表現されます。例えば、C言語の文法はシンボル
c として表現され、c を language 引数としてTree-sitter関数に渡すことができます。
Tree-sitterの言語文法は、動的ライブラリとして配布されています。Emacsで言語文法を使用するには、その動的ライブラリがシステムにインストールされていることを確認する必要があります。Emacsは、以下の順序で複数の場所から言語文法を探します:
treesit-extra-load-path で指定されたディレクトリの一覧において;
user-emacs-directory で指定されたディレクトリ(Init ファイル を参照)の tree-sitter サブディレクトリに;
これらの各ディレクトリにおいて、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 の言語文法が存在し、かつロード可能な場合に non-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 は動的ライブラリのファイル名のベース名(通常は libtree-sitter-language)、function-name はライブラリが提供する関数(通常は tree_sitter_language)です。例えば、
(cool-lang "libtree-sitter-coool" "tree_sitter_cooool")
慣習に従うにはあまりに「クール」すぎると思い込んでいる言語にしては。
この関数は、tree-sitterライブラリがサポートする言語文法アプリケーションバイナリインターフェース(ABI)のバージョンを返します。デフォルトでは、ライブラリがサポートする最新のABIバージョンを返しますが、min-compatibleがnilでない場合は、ライブラリが引き続きサポート可能な最古のABIバージョンを返します。言語文法ライブラリは、tree-sitterライブラリがサポートする最古のバージョンから最新のバージョンまでのABIバージョン向けにビルドされている必要があります。そうでない場合、ライブラリはそれらを読み込むことができません。
この関数は、Emacsがlanguage用にロードした言語文法ライブラリのABIバージョンを返します。languageが利用できない場合、この関数はnilを返します。
この関数は、language を適切な表示名に変換します。例えば、ruby を “Ruby”、cpp
を “C++” に変換します。
ほとんどの言語には「正規」の名称があり、その表示名は単にシンボル名の先頭文字を大文字にしたものです。「不規則」な名称を持つ言語については、treesit-language-display-name-alist が言語シンボルを表示名にマッピングします。
メジャーモードパッケージが「不規則」という名称の言語を使用している場合は、ロード時に treesit-language-display-name-alist へのマッピングを追加すべきです。
構文木とは、パーサーが生成するものです。構文木では、各ノードがテキストの一部を表し、親子関係によって互いに結ばれています。例えば、ソーステキストが
1 + 2
その構文木は~になり得る
+--------------+
| root "1 + 2" |
+--------------+
|
+--------------------------------+
| expression "1 + 2" |
+--------------------------------+
| | |
+------------+ +--------------+ +------------+
| number "1" | | operator "+" | | number "2" |
+------------+ +--------------+ +------------+
また、これをs式として表現することもできます:
(root (expression (number) (operator) (number)))
root、expression、number、operatorといった名称は、ノードの型を指定します。ただし、構文木内のすべてのノードに型があるわけではありません。型を持たないノードは匿名ノードと呼ばれ、型を持つノードは名前付きノードと呼ばれます。匿名ノードとは、括弧 ‘]’ のような句読点や return
のようなキーワードなど、表記が固定されたトークンのことです。
構文木を解析しやすくするため、多くの言語文法では子ノードにフィールド名を割り当てています。例えば、function_definitionノードがdeclaratorとbodyを持つことがあります:
(function_definition declarator: (declaration) body: (compound_statement))
言語の構文の理解を助け、構文木を利用するLispプログラムのデバッグを行うため、Emacsは現在のバッファにあるソースの構文木をリアルタイムで表示する「explore」モードを提供しています。また、Emacsには、モードライン上のポイントにあるノードの情報を表示する「inspect」モードも備わっています。
このコマンドを実行すると、現在のバッファにあるソースの構文ツリーを表示するウィンドウがポップアップします。ソースバッファ内のテキストを選択すると、構文ツリー表示の対応するノードがハイライトされます。構文ツリーのノードをクリックすると、ソースバッファ内の対応するテキストがハイライトされます。
別のパーサーに切り替えるには、treesit-explorer-switch-parser
を使用します。
このマイナーモードでは、モードラインにポイントから開始するノードが表示されます。例えば、モードラインには以下のように表示されます。
parent field: (node (child (...)))
ここで、node、childなどは点から始まるノードである。parentはnodeの親である。nodeは太字で表示される。field-nameはnodeやchildなどのフィールド名である。
どのノードも点を始点としていない場合(すなわち、点がノードの中間にある場合)、モードラインには点をまたぐ最も手前のノードと、その直上の親ノードが表示されます。
このマイナーモードは、単独でパーサーを生成することはありません。(treesit-parser-list) 内の最初のパーサーを使用します(Tree-sitter パーサーの使用を参照)。
言語文法の作成者は、プログラミング言語の文法を定義します。これにより、パーサーがプログラムテキストから具体的な構文木をどのように構築するかが決まります。構文木を効果的に活用するには、文法ファイルを参照する必要があります。
言語文法のプロジェクトリポジトリでは、文法ファイルは通常 grammar.js となっています。言語文法のホームページへのリンクは、tree-sitterのホームページで確認できます。
文法の定義はJavaScriptで記述されています。 例えば、function_definitionノードにマッチするルールは次のようになります。
function_definition: $ => seq(
$.declaration_specifiers,
field('declarator', $.declaration),
field('body', $.compound_statement)
)
ルールは、単一の引数 $
を受け取り、文法全体を表す関数として表現されます。関数自体は他の関数によって構成されており、seq
関数は子要素のシーケンスをまとめ、field
関数は子要素にフィールド名を付与します。上記の定義を、いわゆる バッカス・ナウア形式 (BNF) 構文で記述すると、次のようになります。
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)rule を指定すると、単一のリーフノードが生成されます。つまり、個別の子ノードを持つ親ノードを生成するのではなく、すべてが単一のリーフノードにまとめられます。ノードの取得を参照してください。
token.immediate(rule)通常、文法規則では直前の空白は無視されますが、これにより rule は直前に空白がない場合にのみマッチするようになります。
prec(n, rule)rule にレベル n の優先度を与える。
prec.left([n,] rule)rule を左結合としてマークし、必要に応じてレベル n を付与する。
prec.right([n,] rule)rule を右結合とし、必要に応じてレベル n を指定します。
prec.dynamic(n, rule)これは prec のようなものですが、代わりに実行時に優先順位が適用されます。
Tree-sitterプロジェクトのドキュメントには、文法の記述に関する詳細が記載されています。特に「The Grammar DSL」のセクションをお読みください。