最近、というか昨日からTypedCoffeeScriptの開発再開してAST 気分が盛り上がってるので、簡単なチュートリアルでも。
この記事でやること
- ASTの取得
- ASTの生成
- JavaScript の出力
やらないこと
準備
適当にプロジェクト作ります。
$ mkdir tinyaltjs $ cd tinyaltjs $ npm init # 色々聞かれるけどEnter 連打で良い $ npm install escodegen esprima prettyjson --save
esprima
はJavaScript のコードをASTに変換。 escodegen
は AST から JavaScript を生成。どっちもConstellationさん製 escodegenはConstellationさん製で、彼はesprimaにもコミットしてます。この界隈に来ると基本的に彼のお世話になりっぱなしだと思います。
AST の仕様はこちら。ただ面倒なので読むのはあとででいいです。 Parser API - Mozilla | MDN
prettyjsonはjsonを綺麗に表示するだけのモジュールですが、何かとネストが深いオブジェクトをダンプするのであると便利です。
で、main.coffee で次のようなコードを書く。(※僕が面倒なのでcoffeeを使います)
esprima = require 'esprima' escodegen = require 'escodegen' pj = require 'prettyjson' p = -> console.log pj.render arguments... p esprima.parse "var x = 3"
実行してみましょう。
~/s/tinyaltjs $ coffee main.coffee type: Program body: - type: VariableDeclaration declarations: - type: VariableDeclarator id: type: Identifier name: x init: type: Literal value: 3 raw: 3 kind: var
これが var x = 3
のASTです。
この逆をやります。
ast = type: 'Program' body: [ type: 'VariableDeclaration' declarations: [ type: 'VariableDeclarator' id: type: 'Identifier' name: 'x' init: type: 'Literal' value: 3 raw: 3 ] kind: 'var' ] code = escodegen.generate ast console.log code
~/s/tinyaltjs $ coffee main.coffee var x = 3;
コードが生成できました。
生成されるコードを、ちょっとプログラマブルにしてみましょう。Program は必ず生成されるブロックなので、createProgramNode 関数として抜き出します。
createProgramNode = (body) -> type: 'Program' body: body ast = createProgramNode [ type: 'VariableDeclaration' declarations: [ type: 'VariableDeclarator' id: type: 'Identifier' name: 'x' init: type: 'Literal' value: 3 ] kind: 'var' ]
次に VariableDeclaration を生成する関数に切り出します。
createProgramNode = (body) -> type: 'Program' body: body createVarialbeDelarationNode = -> type: 'VariableDeclaration' declarations: [ type: 'VariableDeclarator' id: type: 'Identifier' name: 'x' init: type: 'Literal' value: 3 ] kind: 'var' ast = createProgramNode [ createVarialbeDelarationNode() ]
ここまで無事に var x = 3; は生成されていますか? これだけじゃ芸がないので、生成する変数名を変えて、ついでに複数行生成してみましょう。
createVarialbeDelarationNode = (variableName) -> type: 'VariableDeclaration' declarations: [ type: 'VariableDeclarator' id: type: 'Identifier' name: variableName init: type: 'Literal' value: 3 ] kind: 'var'
ノードの生成時に変数名を受け取るようにしました。 Program ノードの body は Statement を複数とることができます。
つまり
ast = createProgramNode [ createVarialbeDelarationNode('x') createVarialbeDelarationNode('y') createVarialbeDelarationNode('z') ]
が生成するコードは
~/s/tinyaltjs $ coffee main.coffee var x = 3; var y = 3; var z = 3;
です。
値も変更できるようにしてみましょうか。
その前に文字列と数値の AST を覗いてみましょう。面倒臭いのでrepl でやります。
coffee> esprima.parse('"x"').body[0] { type: 'ExpressionStatement', expression: { type: 'Literal', value: 'x', raw: '"x"' } } coffee> esprima.parse('1').body[0] { type: 'ExpressionStatement', expression: { type: 'Literal', value: 1, raw: '1' } }
Literal を生成するだけなら { type: 'Literal', value: 1}
で良さそうです。
createLiteral = (value) -> type: 'Literal' value: value createVarialbeDelarationNode = (variableName, literal) -> type: 'VariableDeclaration' declarations: [ type: 'VariableDeclarator' id: type: 'Identifier' name: variableName init: literal ] kind: 'var' ast = createProgramNode [ createVarialbeDelarationNode 'x', createLiteral(1) createVarialbeDelarationNode 'y', createLiteral('2') createVarialbeDelarationNode 'z', createLiteral(3) ]
実行
~/s/tinyaltjs $ coffee main.coffee var x = 1; var y = '2'; var z = 3;
まとめ
以降、ぶっちゃけどれだけ大きくなってもやることは変わりません。esprimaで自分が欲しい表現の AST を手に入れて、そのJSONを出力する関数を書けばいいんです。
Altjsは自分で処理系書かなくて良いので、escodegen のおかげでAST を作るとこまでやればあとは適当にjavascript として実行されます。
プログラム言語の体をなしたいなら、構文解析した結果を出力したいコードに対応させる必要があります。それはそれで面倒臭いのですが、peg.js でも使えばよいでしょう
PEG.js – Parser Generator for JavaScript
ここに Lisp の S式のパーサのexampleがあるので、面倒ならそのまま使えばいいと思います。
lisp.js/lisp/grammar/lisp.pegjs at master · devijvers/lisp.js
あとは esprima.parse('console.log("hello")')
とかで関数適用のASTを手に入れてS式の出力結果と照らし合わせて目的の出力を作るだけです!(そこが難しいんですけど!)
AST の細かい仕様は Parser API - Mozilla | MDN で確認すれば良いです。