はじめに
「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。
前回は「+ 演算子」を評価して足し算ができるところまで作りました。今回は「RubyでつくるRuby」の第4章と同じく、四則演算(+, -, *, /)で電卓を作ります。さらに余りや比較が行えるところまでを目指します。
ASTの単純化 simplify()の拡張
simplify()を拡張して、+以外の演算子に対応させます。
function simplify(exp) {
if (exp.type === 'Literal') {
return ['lit', exp.value];
}
if (exp.type === 'BinaryExpression') {
if (exp.operator === '+') {
return ['+', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '-') {
return ['-', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '*') {
return ['*', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '/') {
return ['/', simplify(exp.left), simplify(exp.right)]
}
}
println('-- ERROR: unknown type in simplify) ---');
printObj(exp);
return null;
}
試しに '1+5 - 3*4/2' のASTを渡して見ると、次のツリーが得られます。
[ '-',
[ '+', [ 'lit', 1 ], [ 'lit', 5 ] ],
[ '/', [ '*', [ 'lit', 3 ], [ 'lit', 4 ] ], [ 'lit', 2 ] ] ]
どうやら大丈夫そうです。
四則演算の評価:evaluate()の拡張
同様に実行を担当する evaluate()も拡張して四則演算に対応します。
function evaluate(tree) {
if (tree[0] === 'lit') {
return tree[1];
}
if (tree[0] === '+') {
return evaluate(tree[1]) + evaluate(tree[2]);
}
if (tree[0] === '-') {
return evaluate(tree[1]) - evaluate(tree[2]);
}
if (tree[0] === '*') {
return evaluate(tree[1]) * evaluate(tree[2]);
}
if (tree[0] === '/') {
return evaluate(tree[1]) / evaluate(tree[2]);
}
println('-- ERROR: unknown node in evluate() ---');
printObj(tree);
return null;
}
先ほどの '1+5 - 3*4/2' を実行してみると、答えは 0 (ゼロ)になりました。正解です。
ここまでソース
一旦ここまでのソースはこちらです。この次は演算子を追加します。
// -------------------------
// mininode.js - Node.js by Node.js
// Step4:
// - Calculator +,-,*,/
// -------------------------
const esprima = require("esprima");
// --- parser ----
function parseSrc(src) {
const ast = esprima.parseScript(src);
return ast;
}
function makeTree(ast) {
const exp = ast.body[0].expression;
return simplify(exp);
}
function simplify(exp) {
if (exp.type === 'Literal') {
return ['lit', exp.value];
}
if (exp.type === 'BinaryExpression') {
if (exp.operator === '+') {
return ['+', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '-') {
return ['-', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '*') {
return ['*', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '/') {
return ['/', simplify(exp.left), simplify(exp.right)]
}
}
println('-- ERROR: unknown type in simplify) ---');
printObj(exp);
return null;
}
// --- common ----
function printObj(obj) {
console.dir(obj, {depth: 10});
}
function println(str) {
console.log(str);
}
// --- evaluator ---
function evaluate(tree) {
if (tree[0] === 'lit') {
return tree[1];
}
if (tree[0] === '+') {
return evaluate(tree[1]) + evaluate(tree[2]);
}
if (tree[0] === '-') {
return evaluate(tree[1]) - evaluate(tree[2]);
}
if (tree[0] === '*') {
return evaluate(tree[1]) * evaluate(tree[2]);
}
if (tree[0] === '/') {
return evaluate(tree[1]) / evaluate(tree[2]);
}
println('-- ERROR: unknown node in evluate() ---');
printObj(tree);
return null;
}
// --------
const ast = parseSrc('1 + 5 - 3 * 4 / 2');
const tree = makeTree(ast);
println('--- tree ---');
printObj(tree);
println('--- answer ---');
const answer = evaluate(tree);
println(answer); // expect: (1+5) - (3*4/2) = 6 - 6 = 0
余り(%)と比較演算子を追加
FizzBuzzを作る予定なので、ちょっと先回りして余りを求める % 演算子と、比較のための演算子(===, !==, <, >, <=, >=)を追加しておきます。
まずは simplify() に追加します。
function simplify(exp) {
if (exp.type === 'Literal') {
return ['lit', exp.value];
}
if (exp.type === 'BinaryExpression') {
// ... 省略 ...
if (exp.operator === '%') {
return ['%', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '===') {
return ['===', simplify(exp.left), simplify(exp.right)]
}
if (exp.operator === '!==') {
return ['!==', simplify(exp.left), simplify(exp.right)]
}
// ... まだまだ追加 ...
}
}
とここまで書いていて、すべての演算子で同じパターンなことに気がつきました(遅い)。simplify()はこうしました。
function simplify(exp) {
if (exp.type === 'Literal') {
return ['lit', exp.value];
}
if (exp.type === 'BinaryExpression') {
return [exp.operator, simplify(exp.left), simplify(exp.right)];
}
println('-- ERROR: unknown type in simplify) ---');
printObj(exp);
return null;
}
一方 evaluate() は、一つ一つ処理して行きます。
function evaluate(tree) {
// ... 省略 ...
if (tree[0] === '%') {
return evaluate(tree[1]) % evaluate(tree[2]);
}
if (tree[0] === '===') {
return evaluate(tree[1]) === evaluate(tree[2]);
}
if (tree[0] === '!==') {
return evaluate(tree[1]) !== evaluate(tree[2]);
}
if (tree[0] === '<') {
return evaluate(tree[1]) < evaluate(tree[2]);
}
if (tree[0] === '>') {
return evaluate(tree[1]) > evaluate(tree[2]);
}
if (tree[0] === '<=') {
return evaluate(tree[1]) <= evaluate(tree[2]);
}
if (tree[0] === '>=') {
return evaluate(tree[1]) >= evaluate(tree[2]);
}
println('-- ERROR: unknown node in evluate() ---');
printObj(tree);
return null;
}
これで今後のFizzBuzzも実現できそうです。
次回
ここまでのソースコード
Step4までの全体のコードはこちらです。
// -------------------------
// mininode.js - Node.js by Node.js
// Step4:
// - Calculator +,-,*,/, %, ===,!==,<,>,<=,>=
// -------------------------
const esprima = require("esprima");
// --- parser ----
function parseSrc(src) {
const ast = esprima.parseScript(src);
return ast;
}
function makeTree(ast) {
const exp = ast.body[0].expression;
return simplify(exp);
}
function simplify(exp) {
if (exp.type === 'Literal') {
return ['lit', exp.value];
}
if (exp.type === 'BinaryExpression') {
return [exp.operator, simplify(exp.left), simplify(exp.right)];
}
println('-- ERROR: unknown type in simplify) ---');
printObj(exp);
return null;
}
// --- common ----
function printObj(obj) {
console.dir(obj, {depth: 10});
}
function println(str) {
console.log(str);
}
// --- evaluator ---
function evaluate(tree) {
if (tree[0] === 'lit') {
return tree[1];
}
if (tree[0] === '+') {
return evaluate(tree[1]) + evaluate(tree[2]);
}
if (tree[0] === '-') {
return evaluate(tree[1]) - evaluate(tree[2]);
}
if (tree[0] === '*') {
return evaluate(tree[1]) * evaluate(tree[2]);
}
if (tree[0] === '/') {
return evaluate(tree[1]) / evaluate(tree[2]);
}
if (tree[0] === '%') {
return evaluate(tree[1]) % evaluate(tree[2]);
}
/*
if (tree[0] === '**') {
return evaluate(tree[1]) ** evaluate(tree[2]);
}
*/
if (tree[0] === '===') {
return evaluate(tree[1]) === evaluate(tree[2]);
}
if (tree[0] === '!==') {
return evaluate(tree[1]) !== evaluate(tree[2]);
}
if (tree[0] === '<') {
return evaluate(tree[1]) < evaluate(tree[2]);
}
if (tree[0] === '>') {
return evaluate(tree[1]) > evaluate(tree[2]);
}
if (tree[0] === '<=') {
return evaluate(tree[1]) <= evaluate(tree[2]);
}
if (tree[0] === '>=') {
return evaluate(tree[1]) >= evaluate(tree[2]);
}
println('-- ERROR: unknown node in evluate() ---');
printObj(tree);
return null;
}
// --------
const ast = parseSrc('1 % 2');
const tree = makeTree(ast);
printObj(tree);
const answer = evaluate(tree);
println(answer); // expect: 1
printObj(evaluate(makeTree(parseSrc('1 < 2')))); // expect: true
printObj(evaluate(makeTree(parseSrc('1 > 2')))); // expect: false
printObj(evaluate(makeTree(parseSrc('1 === 2')))); // expect: false
printObj(evaluate(makeTree(parseSrc('1 !== 2')))); // expect: true
printObj(evaluate(makeTree(parseSrc('1 >= 2')))); // expect: false
printObj(evaluate(makeTree(parseSrc('1 <= 2')))); // expect: true