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の文法は存在しません。
なぜこれらのプラグイン1は文法の拡張ができているのでしょうか?
ソースコードを読んで絶望しました。 これらは、Babylon本体が提供しているのです。 ズルい・・・
同じような方法で拡張しようにも、BabylonのParserや文法拡張pluginsを登録する配列はexportされていないので、どうにも手を出せないことが確定しました。
Parserはどのようにソースコードをパースしているのでしょうか。 構文解析といえば、非常に重要な要素として、式と文があり、それぞれに対応したParserが用意されています。
どちらも、 Parser.prototype (変数 pp )に様々なメソッドを登録しています。
式は評価した結果を利用するもの、文はそれ以外2と捉えればだいたいあってます。 プログラム自体は、文を繋げたものです。
const a = 2;
const b = a * a;
if(a > b) {
console.log(b);
}この場合 const a = 2; 、 const b = a * a;、 if ~~~, console.log(b) が文であり、2、 a * a、 a > b, b が式です。
式と文は、言語によっても大きく違います。
ECMAScriptでは a = 1 を評価すると undefined ですが、 Rubyでは文も評価値を持ち(あるいはすべてが式であり)、 a = 1 を評価すると 1 になります。
Rubyでは、
a = if true
'hello'
else
'world'
end
puts a
#=> 'hello'と書けますが、
const a = if (true) {
return 'hello'; // そもそもifの中にreturnは書けない
} else {
return 'world';
}のようには書けません。
プログラムは文の集合なので、parseStatementを見てみましょう。
名前から、文をパースする一番大事なメソッドだと考えられます。
この中では、switch 文で starttype により処理を分割しています。
上から brake や continue、 do や for などお馴染みのキーワードが並んでいますね。
それぞれ別の parseXXXX メソッドに引き継いでいます。
parseXXX は、構文チェックや、さらに別の parseXXX を呼ぶなどして、構文木を作っていきます。
例えば if を処理する parseIfStatement は、まず parseParenExpression を呼び、次に続く(はずの)( 式 ) をパースし、ifの構文木の test というプロパティに設定します。
parseParenExpression の中では、this.expect(tt.parenL) という処理で、次の文字が ( であることを期待しています。
もし次に続くものが ( でない場合、このメソッドが例外を吐くことで、文法の正しさをチェックしています。
次に、parseStatement を呼んで consequent というプロパティに設定しています。
ここまで来たらもう後の部分も解読できるでしょう。
全体を読むと if のノードは、 if ( 式 ) 文 else文? の形式であることが分かります。
もちろん、 { 文; 文; 文; } も文、 if ... も文なので、みなさんのよく知っている、
if ( 式 ) {
文; 文; 文;
} else if (式) {
文; 文; 文;
} else if (式)
文
else 文という記述3がパースできるのです。
} else if (...) { は、 else までが最初の parseIfStatement で、次の if からは新たな parseStatement から呼ばれた 次の parseIfStatement なんですね。
function という構文を処理してみましょう。
ECMAScriptで function を書くパターンはいくつかあります。
function foo(a, b) {
return a + b;
}
const bar = function(a, b) {
return a - b;
}上はfunction文であり、下はfunction式です。
試しにリポジトリ内を parseFunction で検索してみると、statement.js には parseFunctionStatement、 expression.js には parseFunctionExpression が見つかります。
完全に予測可能な結果です。驚き最小の原則に従ってうまく設計されていますね。
どちらも内部で最終的に parseFunction を呼んでいます4。
parseFunction の内部では、いろいろな条件はさくっと無視すると parseFunctionParams と parseFunctionBody を呼び出していることが分かります。
ここでおもむろに flow文法プラグインに意識を飛ばしてみましょう。
先ほどの parseFunctionParams や parseFunctionBody で検索してみてください。
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 側でがんばるという選択肢が残っています5。
次に、実際にパースするところを拡張します。
文法を拡張する程度ならflowプラグインの実装が参考になるでしょう。
新しいキーワードを実装するなら、基本的に parseStatement や parseExpression から攻めていくのが良さそうです。
ちょうど、asyncがやっているような感じです。
export default function(instance) {
instance.extend("parseStatement", function(inner) {
return function(declaration, topLevel) {
const starttype = this.state.type;
const node = this.startNode();
switch(starttype) {
case tt.name:
if (this.state.value === 'unless') {
return this.parseIfStatement(node);
}
}
return inner.call(this, declaration, topLevel);
}
});これで、 unless の文字列を見た場合、それ以降を IfStatement としてパースしてくれるようになりそうです6。
もちろん、 parseXXX を追加すれば、どんな文法でもパースできるようになるはずです。