読者です 読者をやめる 読者になる 読者になる

最小最速で作るaltjs

最近、というか昨日からTypedCoffeeScriptの開発再開してAST 気分が盛り上がってるので、簡単なチュートリアルでも。

この記事でやること

やらないこと

準備

適当にプロジェクト作ります。

$ mkdir tinyaltjs
$ cd tinyaltjs
$ npm init # 色々聞かれるけどEnter 連打で良い
$ npm install escodegen esprima prettyjson --save

esprimaJavaScript のコードを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 で確認すれば良いです。