はじめに
「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。
前回は配列とハッシュ(連想配列)を利用できるようにしました。今回はブートストラップの障害になる箇所を修正します。
予想された引っかかりポイント
ブートストラップを実現するには、ミニNode.jsがサポートしている範囲でミニNode.js(以下mininode)のコードを書かなくてはなりません。が、途中のステップですでにそれに反しているところがありました。
- (1) const の利用
- (2) オブジェクト(のプロパティ、メソッド)の利用
(1) constの利用
変数の宣言は let のみの方針で来ていたのですが、mininodeではconstを使っていました。対処方は次の3通りが考えられます。
- mininode の const をやめて、let に置き換える
- constも真面目に対応する(値の再代入はエラーにする)
- constを不真面目に対応する(let と同じ扱いにする)
今回は3番目のなんちゃってconst対応にしました。evaluate()の該当箇所は次の通りです。
function evaluate(tree, genv, lenv) {
// ... 省略 ...
if (exp.type === 'VariableDeclaration') {
if (exp.kind === 'let') {
const name = exp.declarations[0].id.name;
const val = simplify(exp.declarations[0].init);
return ['var_decl', name, val];
}
// --- STEP 10 ---
if (exp.kind === 'const') {
const name = exp.declarations[0].id.name;
const val = simplify(exp.declarations[0].init);
return ['var_decl', name, val]; // handle const as let
}
println('-- ERROR: unknown kind of decralation in simplify()) ---');
printObj(exp);
abort();
}
// ... 省略 ...
}
かなり無理矢理ですが、ひとまず constは通るようになります。
(2) オブジェクトの利用
こちらはちょっと厄介です。 console.log()のようなオブジェクトのメソッドを使っていますし、ASTを簡略化する simplify()でも、exp.declarations[0].init のようにオブジェクトのメンバーにアクセスしています。これを完全に排除するのは無理ですが、オブジェクトをサポートするのもかなり大変そうです。
どうしたものかと思案していると、ヒントは「RubyでつくるRuby」にありました。minirubyではsimplify()は外部モジュール(gem)になっていて、ブートストラップ対象から除外されています。同じように、mininodeでもオブジェクトを使っている部分はモジュールとして外に追い出してしまうことにします。(ちょっとズルイ気もしますが、オブジェクトのサポートは将来に先送りです)
追い出す対象はソースコードをパースする部分、print系の組み込み関数、そして組み込み関数を呼び出す処理です。
- loadAndParseSrc() ... ソースファイルを読み込んでパース、簡単化したtreeを返す
- println() ... 「RubyでつくるRuby」のp()に相当する1行表示関数
- printObj() ... オブジェクトをたどって階層的に表示する「RubyでつくるRuby」のpp()に相当する関数
- abort() ... 処理を中断する
- callBuiltinByName() ... 組み込み関数の呼び出し。もともとあった callBuiltin() から少し変更
モジュール化でオブジェクトの利用を回避
必要な関数を1つのモジュールにしたいと思いましたが、そうすると結局オブジェクトの関数を呼び出すか、その参照を利用できるように対応しなければなりません。それも回避するため、エクスポートする関数ごとに別々のモジュールにすることにしました。
表示用 println()
// === exports ---
module.exports = println;
function println(str) {
console.log(str);
return null;
}
処理中断 abort()
// === exports ---
module.exports = abort;
function abort() {
process.exit(1);
}
パースを行う loadAndParseSrc()
'use strict'
const esprima = require("esprima");
const fs = require('fs');
const println = require('./module_println.js');
const printObj = require('./module_printobj.js');
const abort = require('./module_abort.js');
// === exports ---
// --- parser ----
module.exports = loadAndParseSrc;
let _argIndex = 2;
function loadAndParseSrc() {
printObj(process.argv);
const filename = process.argv[_argIndex];
println('Loading src file:' + filename);
_argIndex = _argIndex + 1;
const src = loadSrcFile(filename);
const ast = parseSrc(src);
println('-- AST ---');
//printObj(ast);
const tree = makeTree(ast);
//println('-- tree ---');
//printObj(tree);
return tree;
};
// ===== internal functions ====
function loadSrcFile(filename) {
const src = fs.readFileSync(filename, 'utf-8');
return src;
}
function parseSrc(src) {
const ast = esprima.parseScript(src);
return ast;
}
function makeTree(ast) {
// ... 省略 ...
}
function simplify(exp) {
// ... 省略 ...
}
ここで _argIndex という謎の変数が登場していますが、ブートストラップを行う際にコマンドライン引数の該当箇所を見るための小細工です。次のように起動した際に、最初のmininode.js (argv[1]) は 2番目のmininode.js(argv[2])を読み、2番目のmininode.jsからはその次のsample.js(argv[3])を読みに行くようにしています。
$ node mininode.js mininode.js sample.js
組み込み関数を呼び出す callBuiltinByName()
もともとは callBuiltin(func, args)となっていて、関数オブジェクトの参照を渡して呼び出す関数でした。呼び出し元で関数オブジェクトを直接扱わないようにするため、callBuiltinByName(funcName, args)という関数名を渡す方法に変えました。
関数オブジェクトの参照をモジュール側で保持していて、名前を指定されたら変換してから呼び出します。
const loadAndParseSrc = require('./module_parser.js');
const println = require('./module_println.js');
const printObj = require('./module_printobj.js');
const abort = require('./module_abort.js');
// === exports ===
module.exports = callBuiltinByName;
function callBuiltinByName(name, args) {
//const func = eval(name); // OK
const func = builtins[name]; // OK
return func.apply({}, args); // 1st:this, 2nd:args
}
let builtins = {
'require' : require,
'println' : println,
'printObj' : printObj,
'abort' : abort,
'callBuiltinByName' : callBuiltinByName,
'loadAndParseSrc' :loadAndParseSrc,
};
これに伴い、呼び出し元(mininode側)での関数一覧は、名前で持つように変えました。もちろんcallBuiltin()を使っていた場所はcallBuiltinByName()を使うように変更です。
// ---- STEP 10 ---
const loadAndParseSrc = require('./module_parser.js');
const println = require('./module_println.js');
const printObj = require('./module_printobj.js');
const abort = require('./module_abort.js');
const callBuiltinByName = require('./module_builtin.js');
// --- evaluator ---
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') {
// --- STEP 10 ----
// -- call builtin funciton --
//return callBuiltin(mhd[1], args);
return callBuiltinByName(mhd[1], args);
}
// ... 省略 ...
}
// ... 省略 ...
}
// --- 変更後 ---
let genv = {
// --- STEP 10 ---
'require' : ['builtin', 'require'],
'println' : ['builtin', 'println'],
'printObj' : ['builtin', 'printObj'],
'abort' : ['builtin', 'abort'],
'callBuiltinByName' : ['builtin', 'callBuiltinByName'],
'loadAndParseSrc' : ['builtin', 'loadAndParseSrc'],
};
/*
// --- 変更前 ---
let genv = {
'println' : ['builtin', console.log],
'printObj' : ['builtin', printObj],
'abort' : ['builtin', abort],
'require' : ['builtin', require],
};
*/
動かして見る
FizzBuzzを実行
Step8で作ったFizzBuzzが動くことを確認しておきます。
$ node mininode_step10.js fizzbuzz8.js
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
大丈夫そうです。
ブートストラップにチャンレンジ
それでは、いよいよブートストラップにチャレンジです。ドキドキします。
$ node mininode_step10.js mininode_step10.js fizzbuzz8.js
-- ERROR: unknown node in evluate() ---
[ 'func_def',
'fizzbuzz',
...省略 ...
あれ?? うまくいきません。
そして、ここからデバッグに苦しむことになりました。