@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Node.jsでつくるNode.js その1

ledsun.hatenablog.com

の続きです。Node.jsで動くJavaScriptインタプリタを実装しようとする試みです。

作戦

  • パーサにはEsprimaを使う
  • TDD的なスモールスタート戦略で進める(最初はセルフホスティングを意識しない)

下調べ

EsprimaがどのようなASTを返すか確認します。

準備

Esprimaをインストールします。

npm init -y
npm install esprima

ASTを見る

REPLでパース結果を確認します。

nodeコマンドでREPLを起動し

~ node
> const esprima = require('esprima')
undefined
> const util = require('util')
undefined
> console.log(util.inspect(esprima.parse('1 + 1'), false, null))
Script {
  type: 'Program',
  body:
   [ ExpressionStatement {
       type: 'ExpressionStatement',
       expression:
        BinaryExpression {
          type: 'BinaryExpression',
          operator: '+',
          left: Literal { type: 'Literal', value: 1, raw: '1' },
          right: Literal { type: 'Literal', value: 1, raw: '1' } } } ],
  sourceType: 'script' }
undefined

木構造JSONが帰ってきます。

1 + 1を実行する

const esprima = require('esprima')
const util = require('util')

console.assert(test('1 + 1') === 2)

function test(expresssion) {
  const parsed = esprima.parse(expresssion)

  console.log(util.inspect(parsed, false, null))

  const body = parsed.body
  for (const statement of body) {
    return evaluate(statement)
  }
}

function evaluate(statement) {
  switch (statement.type) {
    case 'ExpressionStatement':
      switch (statement.expression.type) {
        case 'BinaryExpression':
          switch (statement.expression.operator) {
            case '+':
              let left;
              if (statement.expression.left.type === 'Literal') {
                left = statement.expression.left.value
              } else {
                console.log(`unknown type ${statement.expression.left.type}`);
              }
              let right;
              if (statement.expression.right.type === 'Literal') {
                right = statement.expression.right.value
              } else {
                console.log(`unknown type ${statement.expression.right.type}`);
              }
              return left + right
              break;
            default:
              console.log(`unknown operator ${statement.expression.operator}`);
          }
          break;
        default:
          console.log(`unknown expression ${statement.expression}`);
      }
      break;
    default:
      console.log(`unknown type ${statement.type}`);
  }
}
  • console.assertを使って実行結果を評価
  • ASTを表示(見ながら実装を進めたい)
  • test関数でスクリプトを実行
  • evaluate関数で文を実行(「RubyでつくるRuby」の最終形に引きづられた、evaluateStatementがベター?)
  • for ofもTemplate literalも使う(現時点でセルフホスティングは考えない)
  • 二項分岐なのにswitchを使った(「RubyでつくるRuby」の最終形に引きづられた、ifで十分)
  • ;有無は統一していない(普段は無し派、ESLintの助けが必要)

実行すると

~ node .
Script {
  type: 'Program',
  body:
   [ ExpressionStatement {
       type: 'ExpressionStatement',
       expression:
        BinaryExpression {
          type: 'BinaryExpression',
          operator: '+',
          left: Literal { type: 'Literal', value: 1, raw: '1' },
          right: Literal { type: 'Literal', value: 1, raw: '1' } } } ],
  sourceType: 'script' }

ASTを表示するだけです。 結果が間違っているときは、console.assertで引っかかってAssertionErrorがでます。

とりあえずここまでです。 次は対応するoperator(-, *, /)を増やします。