JavaScript
Node.js

Node.jsでつくるNode.js - Step 9: 配列とハッシュ(連想配列)を使う

はじめに

「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。

前回はユーザ定義関数を利用できるようにしました。。次の目標であるブートストラップまではまだ遠いので、今回は配列とRubyで言うところのハッシュ(JavaScriptの連想配列)を実装します。(今回は「RubyでつくるRuby」の第9章の前半をベースにしています)

配列を使う

配列を利用したケースのAST

配列の宣言と初期代入、参照、再代入処理を含む、次のソースを用意します。

let ar = [1, 2];
ar[0];
ar[1] = 5;

esprimaを通すと、ASTの配列を新規に作る処理の該当箇所はこうなってます。

             ArrayExpression {
               type: 'ArrayExpression',
               elements:
                [ Literal { type: 'Literal', value: 1, raw: '1' },
                  Literal { type: 'Literal', value: 2, raw: '2' } ] }

ArrayExpressionを解釈すれば良さそうです。
そして配列の要素の参照に該当する箇所はこちら。

        ComputedMemberExpression {
          type: 'MemberExpression',
          computed: true,
          object: Identifier { type: 'Identifier', name: 'ar' },
          property: Literal { type: 'Literal', value: 0, raw: '0' } }

要素の参照はMemberExpressionになっています。
そして要素への代入はこちら。

        AssignmentExpression {
          type: 'AssignmentExpression',
          operator: '=',
          left:
           ComputedMemberExpression {
             type: 'MemberExpression',
             computed: true,
             object: Identifier { type: 'Identifier', name: 'ar' },
             property: Literal { type: 'Literal', value: 1, raw: '1' } },
          right: Literal { type: 'Literal', value: 5, raw: '5' } }

AssignmentExpressionの左辺にMemberExpressionが来ています。通常の変数への代入と区別する必要がありそうです。

simplify()を配列に対応させる

次のtreeを目標にします。

[ 'stmts',
  [ 'var_decl', 'ar', [ 'ary_new', [ 'lit', 1 ], [ 'lit', 2 ] ] ],
  [ 'ary_ref', [ 'var_ref', 'ar' ], [ 'lit', 0 ] ],
  [ 'ary_assign', [ 'var_ref', 'ar' ], [ 'lit', 1 ], [ 'lit', 5 ] ] ]

配列の宣言と、要素の参照に関する拡張はこうしました。

function simplify(exp) {
  // ... 省略 ...

  if (exp.type === 'ArrayExpression') {
    const astElements = exp.elements;
    let treeElements = [];
    let i = 0;
    while (astElements[i]) {
      treeElements[i] = simplify(astElements[i]);
      i = i + 1;
    }

    const tree = ['ary_new'].concat(treeElements);
    return tree;
  }
  if (exp.type === 'MemberExpression') {
    const name = simplify(exp.object);
    const prop = simplify(exp.property);
    const tree = ['ary_ref', name, prop];
    return tree;
  }

  // ... 省略 ...
}

要素への代入は、すでにある変数への代入の部分を拡張します。

function simplify(exp) {
  // ... 省略 ...

  if (exp.type === 'AssignmentExpression') {
    // --- 変数への代入 ---
    if (exp.left.type === 'Identifier') {
      const name = exp.left.name;
      const val = simplify(exp.right);
      return ['var_assign', name, val];  
    }

    // --- 配列の要素の代入 ---
    if (exp.left.type === 'MemberExpression') {
      const name = simplify(exp.left.object);
      const prop = simplify(exp.left.property)
      const val = simplify(exp.right);
      return ['ary_assign', name, prop, val];
    }

    // --- 不明な代入 ---
    println('-- ERROR: unknown type of AssignmentExpression in simplify()) ---');
    printObj(exp);
    abort();
  }

  // ... 省略 ...
}

実行 evaluate()を配列向けに拡張

配列の宣言、参照、代入の処理はこう拡張しました。

function evaluate(tree, genv, lenv) {
  // ... 省略 ...

  if (tree[0] === 'ary_new') {
    let ary = [];
    i = 0;
    while (tree[1 + i]) {
      ary[i] = evaluate(tree[1 + i], genv, lenv);
      i = i + 1;
    }

    return ary;
  }
  if (tree[0] === 'ary_ref') {
    const ary = evaluate(tree[1], genv, lenv);
    const idx = evaluate(tree[2], genv, lenv);
    return ary[idx];
  }
  if (tree[0] === 'ary_assign') {
    const ary = evaluate(tree[1], genv, lenv);
    const idx = evaluate(tree[2], genv, lenv);
    const val = evaluate(tree[3], genv, lenv);
    ary[idx] = val;
    return val;
  }

  // ... 省略 ...
}

ハッシュを使う

ハッシュのASTを見てみる

配列の宣言、参照、要素の代入を行う次のソースを用意します。

let h = { 'a' :1, 'b':2 };
h['a'];
h['b'] = 22;

ハッシュを新規に作る部分のASTはこちら。ObjectExpressionが新登場です。

             ObjectExpression {
               type: 'ObjectExpression',
               properties:
                [ Property {
                    type: 'Property',
                    key: Literal { type: 'Literal', value: 'a', raw: '\'a\'' },
                    computed: false,
                    value: Literal { type: 'Literal', value: 1, raw: '1' },
                    kind: 'init',
                    method: false,
                    shorthand: false },
                  Property {
                    type: 'Property',
                    key: Literal { type: 'Literal', value: 'b', raw: '\'b\'' },
                    computed: false,
                    value: Literal { type: 'Literal', value: 2, raw: '2' },
                    kind: 'init',
                    method: false,
                    shorthand: false } ] }

ハッシュの要素の参照のASTは、配列と同じ形。

        ComputedMemberExpression {
          type: 'MemberExpression',
          computed: true,
          object: Identifier { type: 'Identifier', name: 'h' },
          property: Literal { type: 'Literal', value: 'a', raw: '\'a\'' } }

そして代入のASTも、配列と同じ形です。

     ExpressionStatement {
       type: 'ExpressionStatement',
       expression:
        AssignmentExpression {
          type: 'AssignmentExpression',
          operator: '=',
          left:
           ComputedMemberExpression {
             type: 'MemberExpression',
             computed: true,
             object: Identifier { type: 'Identifier', name: 'h' },
             property: Literal { type: 'Literal', value: 'b', raw: '\'b\'' } },
          right: Literal { type: 'Literal', value: 22, raw: '22' } } }

simplify()のハッシュ対応

ハッシュを新しく作る部分だけ追加すればOKです。

function simplify(exp) {
  // ... 省略 ...

  if (exp.type === 'ObjectExpression') {
    const astProps = exp.properties;
    let treeElements = [];
    let i = 0;
    while (astProps[i]) {
      const key = simplify(astProps[i].key);
      const val = simplify(astProps[i].value)
      treeElements[i * 2] = key;
      treeElements[i * 2 + 1] = val;
      i = i + 1;
    }

    const tree = ['hash_new'].concat(treeElements);
    return tree;
  }

  // ... 省略 ...
}

実行 evaluate() のハッシュ向け拡張

こちらもハッシュを新規に作る処理だけ追加対応します。参照や代入は配列と同じコードで動いてしまいます。ラッキーですね。(そうなるように、単純化したtreeが表現されているのですね)

function evaluate(tree, genv, lenv) {
  // ... 省略 ...

  if (tree[0] === 'hash_new') {
    let hsh = {};
    let i = 1;
    while (tree[i]) {
      const key = evaluate(tree[i], genv, lenv);
      const val = evaluate(tree[i + 1], genv, lenv);
      hsh[key] = val;
      i = i + 2;
    }
    return hsh;
  }

  // ... 省略 ...
}

次回は

配列、ハッシュ(連想配列)ときて、「RubyでつくるRuby」ではブートストラップが実現しています。残念ながら今回作っているミニNode.jsでは、まだブートストラップはできません。次回はその課題を潰していく予定です。

ここまでのソース

GitHubにあげておきます。