Parse

Parseは、ソースコードを構文木に変換する作業です。 また、そのような作業をするソフトウェアを Parser といいます。 Babelが利用するParserは、Babylonというパッケージにまとまっています。

さて、ここで大変残念なお知らせがあります。 実はこの本の目的のひとつとして、Parserを拡張して新しい構文を追加する ことを設定していました。 しかし、ソースコードを読んでみたところ、構文を変更することは完全に無理ということが分かりました。

しかし、世の中にはどう考えてもECMAScriptではない文法を導入しているプラグインがあります。 型を導入する flow やReactの jsx などです。

class Button extends Component {
    render() {
        const text: string = this.state.text;
        return <div>{text}</div>;
    }
}

もちろん、ECMAScriptの仕様には(今のところ) text: string のようなflowの文法や、 <div></div>; のようなjsxの文法は存在しません。 なぜこれらのプラグインは文法の拡張ができているのでしょうか?

ソースコードを読んで絶望しました。 これらは、Babylon本体が提供しているのです。 ズルい・・・

同じような方法で拡張しようにも、BabylonのParserや文法拡張pluginsを登録する配列はexportされていないので、どうにも手を出せないことが確定しました。

Parserの動作を読んでみる

Parserはどのようにソースコードをパースしているのでしょうか。 構文解析といえば、非常に重要な要素として、があり、それぞれに対応したParserが用意されています。

どちらも、 Parser.prototype (変数 pp )に様々なメソッドを登録しています。

式と文

式は評価した結果を利用するもの、文はそれ以外と捉えればだいたいあってます。 プログラム自体は、文を繋げたものです。

この場合 const a = 2;const b = a * a;if ~~~, console.log(b) が文であり、2a * aa > b, b が式です。

式と文は、言語によっても大きく違います。 ECMAScriptでは a = 1 を評価すると undefined ですが、 Rubyでは文も評価値を持ち(あるいはすべてが式であり)、 a = 1 を評価すると 1 になります。

Rubyでは、

と書けますが、

のようには書けません。

Statementを読んでみる

プログラムは文の集合なので、parseStatementを見てみましょう。 名前から、をパースする一番大事なメソッドだと考えられます。

この中では、switch 文で starttype により処理を分割しています。 上から brakecontinuedofor などお馴染みのキーワードが並んでいますね。 それぞれ別の parseXXXX メソッドに引き継いでいます。

parseXXX は、構文チェックや、さらに別の parseXXX を呼ぶなどして、構文木を作っていきます。

if 文

例えば if を処理する parseIfStatement は、まず parseParenExpression を呼び、次に続く(はずの)( 式 ) をパースし、ifの構文木の test というプロパティに設定します。 parseParenExpression の中では、this.expect(tt.parenL) という処理で、次の文字が ( であることを期待しています。 もし次に続くものが ( でない場合、このメソッドが例外を吐くことで、文法の正しさをチェックしています。

次に、parseStatement を呼んで consequent というプロパティに設定しています。 ここまで来たらもう後の部分も解読できるでしょう。 全体を読むと if のノードは、 if ( 式 ) 文 else文? の形式であることが分かります。 もちろん、 { 文; 文; 文; } も文、 if ... も文なので、みなさんのよく知っている、

という記述がパースできるのです。 } else if (...) { は、 else までが最初の parseIfStatement で、次の if からは新たな parseStatement から呼ばれた 次の parseIfStatement なんですね。

function

function という構文を処理してみましょう。 ECMAScriptで function を書くパターンはいくつかあります。

上はfunction文であり、下はfunction式です。 試しにリポジトリ内を parseFunction で検索してみると、statement.js には parseFunctionStatementexpression.js には parseFunctionExpression が見つかります。 完全に予測可能な結果です。驚き最小の原則に従ってうまく設計されていますね。 どちらも内部で最終的に parseFunction を呼んでいます。 parseFunction の内部では、いろいろな条件はさくっと無視すると parseFunctionParamsparseFunctionBody を呼び出していることが分かります。

文法拡張プラグインの正体

ここでおもむろに flow文法プラグインに意識を飛ばしてみましょう。

先ほどの parseFunctionParamsparseFunctionBody で検索してみてください。 instance.extend という処理で、それぞれのメソッドがオーバーライドされています。

parseFunctionParams の場合は、 (function fooの)次に < の文字が来たら、flowのテンプレート宣言 function foo<T> ( ... ) ... であると判断し <T> の処理をし、残りの ( 以降を元のメソッドに引き継いでいます。 parseFunctionBody の場合は、 (Paramsの)次に ':' の文字が来たら、 メソッドの戻り値の型を宣言する : number { ... であると判断し、 : number の処理をし、 残りの { 以降を元のメソッドに引き継いでいます。

他にも様々なメソッドが同様の方法でオーバーライドされ、 flowの文法を受理できるように拡張されています。

文法を拡張するには

ユーザが文法拡張することは現状できません。 Parserさえexportされればできるという感じでもなさそうです。 ここから先に出てくるコードは、実際には試せていないので全て妄想です。

まず、文法に新しいキーワード(ifやfunctionなどの予約語)を追加したい場合、Tokenizerキーワード登録が必要です。 Tokenizerクラスでは、このキーワード情報をファイルスコープの変数として扱っているので、外部から操作することができません。 キーワード情報をクラスの内部に変数で持つようになればプラグインから操作可能になるでしょう。 このキーワード情報は他のファイルからもいくつか参照されており、同様に操作不可能になっています。 types.js 自体にプラグイン機構がないと無理そうです。

ただ、キーワードに登録できなくても、不明な文字列は全て name というトークンとして処理されるので、parseXXX 側でがんばるという選択肢が残っています。

次に、実際にパースするところを拡張します。 文法を拡張する程度ならflowプラグインの実装が参考になるでしょう。 新しいキーワードを実装するなら、基本的に parseStatementparseExpression から攻めていくのが良さそうです。 ちょうど、asyncがやっているような感じです。

これで、 unless の文字列を見た場合、それ以降を IfStatement としてパースしてくれるようになりそうです。 もちろん、 parseXXX を追加すれば、どんな文法でもパースできるようになるはずです。

Last updated