👓 レキシカル解析と構文解析#
この記事では、私が長い時間をかけて理解した知識を簡単にまとめるだけなので、詳しく説明することはありません。ただ、読者の理解を助けるために、これらのトピックについて少し深く話すことがあります。
レキシカルアナライザ(英語では Lexer または Scanner と呼ばれることが一般的です)は、ソースコードを「トークン」と呼ばれる不可分の要素のリストに分割するために使用されます。
C 言語の例を挙げて説明します。
#include <stdio.h>
int main() {
printf("Hello world!");
}
上記のソースコードに含まれるすべてのトークンは次のとおりです。
<プリプロセッサディレクティブ #include> <スペース> <左尖括弧 '<'> <名前 stdio.h> <右尖括弧 '>'> <改行>
<改行>
<キーワード int> <スペース> <名前 main> <左丸括弧 '('> <右丸括弧 ')'> <スペース> <左波括弧 '{'> <改行>
<スペース> <スペース> <名前 printf> <左丸括弧 '('> <文字列リテラル> <右丸括弧 ')'> <セミコロン ';' > <改行>
<右波括弧 '}'>
レキシカルアナライザの入力は文字列であり、出力はトークンのリストです。各トークンには、位置(Position)、内容(Raw content)、** タイプ(Token Type)** の 3 つの重要な属性があります。位置を記録することは、後でコードにエラーがあった場合に問題を特定するのに役立ちます。
構文解析器(英語では Parser と呼ばれることが一般的です)は、プログラミング言語の設計仕様に基づいて、定義に合致する複数のトークンを組み合わせて構文ノードを作成し、最終的に「抽象構文木」(AST、Abstract Syntax Tree)を作成する作業です。
🥵 テンプレート文字列の処理の難しさ#
以下では、JavaScript のテンプレート文字列構文を例に説明します。
レキサーの実装を始めたとき、入力内容を一つずつ読み取るだけで、対応するトークンを取得できると思っていましたが、テンプレート文字列の場合は異なることに気づきました。
テンプレート文字列をレキシカル解析の段階で「分割できない」トークンとして読み取るのは非常に困難です。明らかに、テンプレート文字列は分割可能です。
`my name is ${"David" + ` - ${firstName}`}, nice to meet you!`
🤔️ どうすればいいですか?#
トークンを順番に読み取るためには、解析の現在の状態を示すいくつかの変数を設定し、通常の文字列解析と同じ方法でテンプレート文字列を処理しないようにする必要があります。
確かなことは、テンプレート文字列の処理は構文解析の段階で行われることであり、次の 2 つの種類の構文ノードを含むことです。
- 分散した複数の文字列テキスト
- 挿入式(式)、挿入式も構文ノードです
interface TemplateStringNode {
quasis: TemplateElement[]
expressions: ExpressionNode[]
}
したがって、最も良い方法は、パーサーにテンプレート文字列の解析を開始するための特定のトークンを見つけ、状態情報に基づいて可能なネストの処理を行うことです。
次の 2 つの重要な情報を設定します。
isReadingText
:テキストを読み取る必要があるかどうかnested
:ネストの深さ
上記の図の推論分析に従って、次の結論に達することができます。
テンプレート文字列の引用符
はisReadingText
の状態を反転させます- 挿入式の開始マーカー
${
はnested
を 1 つ増やし、isReadingText
をfalse
に設定します。なぜなら、読み取るのは挿入式だからです。 右波括弧
はnested
を 1 つ減らし、isReadingText
をtrue
に設定します。なぜなら、テンプレート文字列のテキストの読み取りに戻ったからです。
isReadingText
がfalse
に設定され、nested のレベルが 0 になるまで、テンプレート文字列ノードの解析は終了しません。
参考文献#
以下の 2 つの Stackoverflow の質問に参考になる情報があります: