はじめに
「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) に感銘をうけて、ミニNode.js(以下mininode)を作ってきました。中間で生成している単純化構文木 (Tree)は、基本的にミニRuby(以下miniruby)と同じ形をしています。ということは、それを中間言語と扱ってminiruby用のコードを実行できるのではないでしょうか?
minirubyをmininodeで実行できるか?
一種の中間言語となるTreeが共通なら、minirubyで生成したものをmininodeで実行できるはずです。となると、minirubyはブートストラップ可能なので、そのままmininodeで実行できるのでは? と一瞬期待してしまいました。
が、冷静に考えると組み込み関数として用意された部分はRubyの機能やモジュールに依存していて、mininodeからは実行できません。
そこで、minirubyそのものを実行するのではなく、途中で作られるsimplify()済みのTreeをmininodeで読み込んで実行することに切り替えました。
- rubyコード → minirubyでsimplify() → JSONで保存 → mininodeで読み込み実行(evaluate)
minirubyのsimplify()結果を保存
fizzbuzzのコード
もとになるRubyのFizzBuzzのコードです。ユーザ定義関数を使うようにしました。
def fizzbuzz(n)
if n % 15 == 0
"FizzBuzz"
elsif n % 3 == 0
"Fizz"
elsif n % 5 == 0
"Buzz"
else
n
end
end
i = 1
while i < 100
p(fizzbuzz(i))
i = i + 1
end
Rubyで単純化Treeを保存
先ほどのRubyのコードをパースして単純化します。用意されているgemを使えば簡単ですね。
require "./minruby"
require "json"
# read file
str = minruby_load()
# parse line
tree = minruby_parse(str)
# write to json
json_file_path = 'fizzbuzz_tree.json';
j = { tree: tree };
open(json_file_path, 'w') do |io|
JSON.dump(j, io)
end
実行すると、決め打ちのファイル名 fizzbuzz_tree.json に単純化したTreeがJSON形式で書き込まれます。
$ ruby savetree.rb fizzbuzz_func.rb
保存されたTree
保存された fizzbuzz_tree.json を少し整形して掲載します。
{"tree":
["stmts",
["func_def","fizzbuzz",["n"],
["if",["==",["%",["var_ref","n"],["lit",15]],["lit",0]],
["lit","FizzBuzz"],
["if",["==",["%",["var_ref","n"],["lit",3]],["lit",0]],
["lit","Fizz"],
["if",["==",
["%",["var_ref","n"],["lit",5]],["lit",0]],
["lit","Buzz"],["var_ref","n"]
]
]
]
],
["var_assign","i",["lit",1]],
["while",["<",["var_ref","i"],["lit",100]],
["stmts",
["func_call","p",
["func_call","fizzbuzz",["var_ref","i"]]
],
["var_assign","i",["+",["var_ref","i"],["lit",1]]]
]
]
]
}
mininodeで実行
単純化Tree 読み込む
保存済みTreeを読み込み、それを実行します。
const fs = require('fs');
// ---- Extra1 for miniruby ----
function loadJson(filename) {
const src = fs.readFileSync(filename, 'utf-8');
const json = JSON.parse(src);
return json;
}
const json = loadJson(process.argv[2]);
const tree = json.tree;
const answer = evaluate(tree, genv, lenv);
実際にはこれだけで動かず、いくつか対策が必要でした。
- 変数宣言(var_decl)無しで変数を使えるようにする
- 比較演算子 == を追加
- 組み込み関数 p() を追加
変数宣言無しを認める
function evaluate(tree, genv, lenv) {
// ... 省略 ...
if (tree[0] === 'var_assign') {
// -- check EXIST --
const name = tree[1];
// ---- Extra1 for miniruby ----
/* -- without check --
if (name in lenv) {
lenv[name] = evaluate(tree[2], genv, lenv);
return lenv[name];
}
*/
lenv[name] = evaluate(tree[2], genv, lenv);
return lenv[name];
}
// ... 省略 ...
}
比較演算子 == を追加
function evaluate(tree, genv, lenv) {
// ... 省略 ...
// ---- Extra1 for miniruby ----
if (tree[0] === '==') {
return evaluate(tree[1], genv, lenv) == evaluate(tree[2], genv, lenv);
}
// ... 省略 ...
}
組み込み関数 p(), pp() を追加
let genv = {
// ... 省略 ...
// ---- Extra1 for miniruby ----
'p' : ['builtin', 'println'],
'pp' : ['builtin','printObj'],
};
実行してみる
それでは実行してみましょう。
- minirubyで作った単純化Tree ... miniruby/fizzbuzz_tree.json
- 今回拡張したmininode ... mininode_extra_r.js
とします。
$ node mininode_extra_r.js miniruby/fizzbuzz_tree.json
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
... 省略 ...
94
Buzz
Fizz
97
98
Fizz
実行できました!
まとめ
中間表現となる単純化Treeが共通なminirubyとmininodeは、いわば兄弟言語と言えます。目論見通りminiruby向けのコードを、中間表現を経由して間接的にmininodeで実行することができました。