Rome は、JavaScript と TypeScript のパースに様々な技術を使用しています。このチュートリアルでは、それらを理解しやすい順序で要約しています。
文法
JavaScript の文法は解析が非常に困難なものの一つであり、 このチュートリアルでは私が学習中に経験した苦労と涙を詳細に説明します。
LL(1)文法
Wikipedia によると、
an LL grammar is a context-free grammar that can be parsed by an LL parser, which parses the input from Left to right
最初の「L」はソースを左から右にスキャンする ことを意味し、 2番目の「L」は左端導出木の構築を意味します。
文脈自由であり、LL(1) の「1」は次のトークンを覗き見るだけで木を構築できることを意味します。
LL 文法は、私たちが怠惰な人間であり、パーサを手動で書く必要がないように、プログラムを自動的に生成するプログラムを書きたいという理由で、学術界で特に興味を持たれています。
残念なことに、ほとんどの産業用プログラミング言語には素晴らしい LL(1) 文法はありません。 JavaScript もその例外ではありません。
Mozillaは数年前に jsparagus プロジェクトを開始し、 Python で LALR パーサジェネレータ を作成しました。 彼らは過去2年間ほとんど更新しておらず、js-quirks.md の最後に強いメッセージを送っています。
What have we learned today?
- Do not write a JS parser.
- JavaScript has some syntactic horrors in it. But hey, you don't make the world's most widely used programming language by avoiding all mistakes.
JavaScript を解析する唯一の実用的な方法は、その文法の性質上、手動で再帰下降パーサを書くことです。 そのため、足を撃つ前に文法の特異性をすべて学びましょう。
以下のリストは簡単なものから理解が難しくなりますので、 コーヒーを飲んでゆっくりと時間をかけてください。
識別子
#sec-identifiers
で定義されている識別子には3つのタイプがあります。
IdentifierReference[Yield, Await] :
BindingIdentifier[Yield, Await] :
LabelIdentifier[Yield, Await] :
estree
および一部の AST では、上記の識別子を区別せず、仕様書ではそれらを平文で説明していません。
BindingIdentifier
は宣言であり、IdentifierReference
はバインディング識別子への参照です。
例えば、var foo = bar
の場合、foo
は文法上の BindingIdentifier
であり、bar
は IdentifierReference
です。
VariableDeclaration[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt
Initializer[In, Yield, Await] :
= AssignmentExpression[?In, ?Yield, ?Await]
AssignmentExpression
を PrimaryExpression
にたどると、
PrimaryExpression[Yield, Await] :
IdentifierReference[?Yield, ?Await]
ASTでこれらの識別子を異なる方法で宣言すると、特に意味解析のために、下流のツールを大幅に簡素化することができます。
pub struct BindingIdentifier {
pub node: Node,
pub name: Atom,
}
pub struct IdentifierReference {
pub node: Node,
pub name: Atom,
}
クラスと Strict モード
ECMAScript のクラスは、Strict モードの後に生まれたため、クラス内のすべての要素はシンプルさのために Strict モードである必要があります。
#sec-class-definitions
では、Node: A class definition is always strict mode code.
と述べられています。
関数スコープと関連付けることで Strict モードを宣言することは簡単ですが、class
宣言にはスコープがないため、クラスの解析のために追加の状態を保持する必要があります。
https://github.com/swc-project/swc/blob/f9c4eff94a133fa497778328fa0734aa22d5697c/crates/swc_ecma_parser/src/parser/class_and_fn.rs#L85
レガシーオクタルと Use Strict
#sec-string-literals-early-errors
では、文字列内のエスケープされたレガシーオクタル "\01"
は許可されていません。
EscapeSequence ::
LegacyOctalEscapeSequence
NonOctalDecimalEscapeSequence
このプロダクションにマッチするソーステキストが Strict モードコードである場合、構文エラーです。
これを検出するのに最適な場所は、レキサーの内部です。レキサーはパーサーに Strict モードの状態を尋ね、それに応じてエラーをスローすることができます。
しかし、これはディレクティブと混在した場合には不可能になります。
https://github.com/tc39/test262/blob/747bed2e8aaafe8fdf2c65e8a10dd7ae64f66c47/test/language/literals/string/legacy-octal-escape-sequence-prologue-strict.js#L16-L19
use strict
はエスケープされたレガシーオクタルの後に宣言されていますが、構文エラーがスローされる必要があります。
幸いなことに、実際のコードではディレクティブとレガシーオクタルを組み合わせることはありません...上記の test262 のケースをパスしたい場合を除いては。
非単純パラメータと Strict モード
非Strictモードでは、同じ関数パラメータを許可します function foo(a, a) { }
、そして use strict
を追加することでこれを禁止することができます:function foo(a, a) { "use strict" }
。
その後のes6では、関数パラメータに他の文法が追加されました。例えば function foo({ a }, b = c) {}
。
では、次のようなコードを書いた場合、"01" は Strict モードのエラーとなるのでしょうか?
function foo(value=(function() { return "\01" }())) {
"use strict";
return value;
}
具体的には、パーサーの観点からパラメータ内に Strict モードの構文エラーがある場合、どうすべきでしょうか?
そのため、#sec-function-definitions-static-semantics-early-errors
では、次のように述べてこれを禁止しています。
FunctionDeclaration :
FunctionExpression :
FunctionBodyがFunctionBodyContainsUseStrictでtrueであり、FormalParametersがIsSimpleParameterListでfalseである場合、構文エラーです。