字句解析と構文解析
コンパイラが最初に行うことは、プログラム(UTF-8 Unicode テキスト)を受け取り、 文字列よりもコンパイラが扱いやすいデータ形式に変換することです。 これは、字句解析と構文解析という 2 つの段階で行われます。
- 字句解析 は文字列を受け取り、それをトークンのストリームに変換します。たとえば、
foo.bar + buzは、foo、.、bar、+、buzというトークンに変換されます。 これはrustc_lexerで実装されています。
- 構文解析 はトークンのストリームを受け取り、コンパイラが扱いやすい構造化された形式に変換します。 これは通常、抽象構文木 (AST) と呼ばれます。
AST
AST は、Span を使用して特定の AST ノードを元のソーステキストにリンクしながら、
メモリ内で Rust プログラムの構造を反映します。AST は
rustc_ast で定義されており、トークンとトークンストリームの定義、
AST を変更するためのデータ構造/トレイト、およびコンパイラの他の AST 関連部分
(レキサーやマクロ展開など)の共有定義も含まれています。
AST 内のすべてのノードには、構造体などのトップレベル項目だけでなく、
個々の文や式も含めて、それぞれ独自の NodeId があります。NodeId は、
クレート内の AST ノードを一意に識別する識別子番号です。
しかし、これらはクレート内で絶対的なものなので、AST 内の単一のノードを追加または削除すると、
それ以降のすべての NodeId が変わってしまいます。これにより、
できるだけ変更されるものを少なくしたいインクリメンタルコンパイルにおいて、
NodeId はほとんど役に立たなくなります。
NodeId は、マクロ展開や名前解決など、AST を直接操作する rustc のすべての部分で使用されます
(これらについては、今後の数章でさらに説明します)。
構文解析
パーサーは rustc_parse で定義されており、
レキサーへの高レベルインターフェースと、マクロ展開後に実行されるいくつかの検証ルーチンも含まれています。
特に、rustc_parse::parser にはパーサーの実装が含まれています。
パーサーへの主なエントリポイントは、さまざまな parse_* 関数や
rustc_parse 内のその他の関数を介したものです。これらを使うと、
SourceFile(たとえば単一ファイル内のソース)をトークンストリームに変換し、
そのトークンストリームからパーサーを作成し、その後パーサーを実行して
Crate(ルート AST ノード)を取得するといったことができます。
実行されるコピーの量を最小限に抑えるため、
Lexer と Parser はどちらも、親の ParseSess に束縛されるライフタイムを持ちます。
これには、構文解析中に必要なすべての情報と、SourceMap 自体が含まれています。
構文解析中に、マクロ定義やマクロ呼び出しに遭遇する場合があることに注意してください。 これらは展開されるように脇に置いておきます(マクロ展開を参照)。 展開自体により、マクロの出力を構文解析する必要が生じることがあり、 それによって展開すべきさらなるマクロが明らかになることもあり、これが繰り返されます。
字句解析の詳細
字句解析のコードは、2 つのクレートに分かれています。
-
rustc_lexerクレートは、&strをトークンを構成するチャンクに分割する役割を担います。 レキサーを生成された有限状態機械として実装することは一般的ですが、rustc_lexerのレキサーは手書きです。 -
Lexerは、rustc_lexerをrustc固有のデータ構造と統合します。 具体的には、rustc_lexerが返すトークンにSpan情報を追加し、 識別子をインターンします。