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