LoginSignup
1

More than 5 years have passed since last update.

Node.jsでつくるNode.js - Step 7: 組み込み関数を呼び出す

Last updated at Posted at 2018-06-10

はじめに

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

前回はifによる条件分岐と、whileによる繰り返しを作り、最初の目標のFizzBuzzを動かすところまで実装しました。今回は次の目標であるブートストラップを目指して、組み込み関数の呼び出しを作ります。(今回も「RubyでつくるRuby」の第7章をベースにしています)

関数呼び出しのAST

実はStep 5で手抜きの関数呼び出しを作っていますが、今回は真面目に実装します。まずはASTを見てみましょう。今回読み込ませるソースはこちら。

println('abc');
println(1, 2);

println()という関数は、私が勝手に決めた画面出力用の組み込み関数です。「RubyでつくるRuby」に登場するp()に相当します。

ASTはこちら。

Script {
  type: 'Program',
  body:
   [ ExpressionStatement {
       type: 'ExpressionStatement',
       expression:
        CallExpression {
          type: 'CallExpression',
          callee: Identifier { type: 'Identifier', name: 'println' },
          arguments: [ Literal { type: 'Literal', value: 'abc', raw: '\'abc\'' } ] } },
     ExpressionStatement {
       type: 'ExpressionStatement',
       expression:
        CallExpression {
          type: 'CallExpression',
          callee: Identifier { type: 'Identifier', name: 'println' },
          arguments:
           [ Literal { type: 'Literal', value: 1, raw: '1' },
             Literal { type: 'Literal', value: 2, raw: '2' } ] } } ],
  sourceType: 'script' }

どうやら CallExpression を解釈すれば良さそうです。関数名は callee.nameに、引数はargumentsに複数含まれます。このargumentsを複数扱うのがポイントになりそうです。

AST単純化処理 simplify() の拡張

CallExpression の場合には、種別(func_call)、関数名の後に、引数を並べるようにします。 println(1, 2) の場合に欲しい結果はこうなります。

  [ 'func_call', 'println', [ 'lit', 1 ], [ 'lit', 2 ] ] 

ソースの拡張部分はこうしました。

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

  if (exp.type === 'CallExpression') {
    const name = exp.callee.name;
    const astArgs = exp.arguments;

    // -- for multi args ---
    let i = 0;
    let treeArgs = [];
    while (astArgs[i]) {
      treeArgs[i] = simplify(astArgs[i]);
      i = i + 1;
    }

    const tree = ['func_call', name].concat(treeArgs);
    return tree;
  }

  // ... 省略 ...
}

実行 evaluate() の拡張

組み込み関数の一覧

準備している組み込み関数を、あらかじめハッシュで持っておきます。「RubyでつくるRuby」のまねですが、文字列でなく関数の参照で持つようにしてみました。

let genv = {
  'println' : ['builtin', console.log],
  'printObj' : ['builtin', printObj],
  'abort' : ['builtin', abort],
};

組み込み関数の実体

組み込み関数の実体は、次のように用意しました。

// オブジェクトをダンプ
function printObj(obj) {
  console.dir(obj, {depth: 10});
  return null;
}

// 処理を中断
function abort() {
  process.exit(1);
}

// mininode本体で使うための暫定実装。組み込み関数は、console.logを直接利用
function println(str) {
  console.log(str);
  return null;
}

組み込み関数の呼び出し

実際に関数を呼び出す部分で悩みました。Rubyではsendを使ってオブジェクトにメッセージを送っているようですが、JavaScriptには同じ仕組みはなさそうです(私の知る限り)。今回は apply() を使うことにしました。

function callBuiltin(func, args) {
  return func.apply({}, args); // 1st:this, 2nd:args
}

実行部分の拡張

それでは準備したものを使って、evaluate()を拡張します。まずは evaluate()の引数に用意した genv も渡すように、すべての呼び出し箇所を書き換えます。

// before
evaluate(tree, env)

// after
evaluate(tree, genv, lenv)

そして evaluate() の内部の処理も追加します。

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

  if (tree[0] === 'func_call') {
    const mhd = genv[tree[1]];
    let args = [];
    let i = 0;
    while (tree[2 + i]) {
      args[i] = evaluate(tree[2 + i], genv, lenv);
      i = i + 1;
    }

    if (mhd[0] === 'builtin') {
      // -- call builtin function --
      return callBuiltin(mhd[1], args);
    }

    // ... ユーザー定義関数の呼び出しはまだ ...
    println('--- ERROR, user func NOT supported YET ---');
    abort();
  }

  // ... 省略 ...
}

genv から組み込み関数への参照を取得、引数を新しいハッシュに詰め替えて、callBuiltin()を経由して組み込み関数を呼び出しています。

次回

ここまでのソース

長くなってきたので、GitHubのmininodeに移しました

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1