JavaScript
Node.js

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


はじめに

「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に移しました