はじめに
「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。
前回は四則演算(+, -, *, /)で電卓を作りました。今回は「RubyでつくるRuby」の第5章と同じく、 複数行のソースへの対応と、変数を利用するところまでを目指します。
ソースファイルの読み込み
loadSrcFile()では引数で渡されたファイル名のソースを、fs.readFileSync()で読み込みます。
const fs = require('fs');
function loadSrcFile(filename) {
const src = fs.readFileSync(filename, 'utf-8');
return src;
}
function loadAndParseSrc() {
const filename = process.argv[2];
println('Loading src file:' + filename);
const src = loadSrcFile(filename);
const ast = parseSrc(src);
const tree = makeTree(ast);
return tree;
}
ついでに、起動引数で指定されたファイルの読み込みからパースまで、一気に行う関数loadAndParseSrc()も用意しました。
複数行のサポート
複数行のソースをパースしたときのASTを調べて見ます。
1;
2 + 3;
上のコードを esprima でパースすると、ASTはこうなりました。
Script {
type: 'Program',
body:
[ ExpressionStatement {
type: 'ExpressionStatement',
expression: Literal { type: 'Literal', value: 1, raw: '1' } },
ExpressionStatement {
type: 'ExpressionStatement',
expression:
BinaryExpression {
type: 'BinaryExpression',
operator: '+',
left: Literal { type: 'Literal', value: 2, raw: '2' },
right: Literal { type: 'Literal', value: 3, raw: '3' } } } ],
sourceType: 'script' }
bodyに複数の Statement が含まれているので、これを「RubyでつくるRuby」にならって 'stmts' に束ねます。
[ 'stmts',
[ 'lit', 1 ],
[ '+', [ 'lit', 2 ], [ 'lit', 3 ] ]
]
このために、makeTree()関数を拡張しました。
function makeTree(ast) {
// --- handle multi lines ---
let i = 0;
let exps = [];
while (ast.body[i]) {
const line = ast.body[i].expression;
const exp = simplify(line);
exps[i] = exp;
i = i + 1;
}
// --- single line ---
if (exps.length === 1) {
return exps[0];
}
// --- multi lines ---
const stmts = ['stmts'].concat(exps);
return stmts;
}
実行する方の evaluate()も、同様に拡張します。
function evaluate(tree) {
if (tree[0] === 'stmts') {
let i = 1;
let last;
while (tree[i]) {
last = evaluate(tree[i]);
i = i + 1;
}
return last;
}
// ... 省略 ...
}
試しにこちらの複数行のファイルを読み込ませて実行すると、最後の演算の5が返ってきます。
1;
2 + 3;
変数の扱い
変数を扱うにために、次の3つの場合を考えます。
- 変数の宣言(と初期値の代入): var_decl
- 変数の再代入: var_assign
- 変数の参照: var_ref
「RubyでつくるRuby」では定義と再代入は区別しませんが、今回はあえて区別することにしました。再代入をするには、定義済みであることを条件にします。また定義には let だけを使うことにしました。
ここで最終目標であるブートストラップを考えると、ミニNode.jsの処理で既に const を使っているところが引っかかりそうですが、今は気にせず進めることにします。
変数宣言のAST
初期値なし: let a;
Script {
type: 'Program',
body:
[ VariableDeclaration {
type: 'VariableDeclaration',
declarations:
[ VariableDeclarator {
type: 'VariableDeclarator',
id: Identifier { type: 'Identifier', name: 'a' },
init: null } ],
kind: 'let' } ],
sourceType: 'script' }
初期値あり: let b = 1;
Script {
type: 'Program',
body:
[ VariableDeclaration {
type: 'VariableDeclaration',
declarations:
[ VariableDeclarator {
type: 'VariableDeclarator',
id: Identifier { type: 'Identifier', name: 'b' },
init: Literal { type: 'Literal', value: 1, raw: '1' } } ],
kind: 'let' } ],
sourceType: 'script' }
これをsimplify()に通すのですが、ここで一つ問題が発覚。今までbodyの下にあるのはExpressionStatement だという前提で作っていましたが、VariableDeclaration で違う中身になっています。 simplify()だけでなく、makeTree()も合わせて修正が必要です。
function makeTree(ast) {
// --- handle multi lines ---
let i = 0;
let exps = [];
while (ast.body[i]) {
const line = ast.body[i];
const exp = simplify(line);
exps[i] = exp;
i = i + 1;
}
// --- single line ---
if (exps.length === 1) {
return exps[0];
}
// --- multi lines ---
const stmts = ['stmts'].concat(exps);
return stmts;
}
function simplify(exp) {
if (exp === null) {
return null;
}
if (exp.type === 'ExpressionStatement') {
return simplify(exp.expression);
}
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];
}
println('-- ERROR: unknown kind of decralation in simplify()) ---');
printObj(exp);
abort();
}
// ... 省略 ...
}
VariableDeclarationを扱えるようにして、declarations の最初の要素だけを取り出しています。どうやら複数の要素を持てるようですが、ミニNode.jsでは1つと割り切りました。
また、初期値の有無は init の中身があるかnullか、の違いだったので、同じ形に表現しています。
初期値なしの単純化結果
[ 'var_decl', 'a', null ]
初期値ありの単純化結果
[ 'var_decl', 'b', [ 'lit', 1 ] ]
変数再代入のAST
ASTから変数への再代入の部分だけを取り出すと、このような形になっています。(この場合は a に 1を代入)
ExpressionStatement {
type: 'ExpressionStatement',
expression:
AssignmentExpression {
type: 'AssignmentExpression',
operator: '=',
left: Identifier { type: 'Identifier', name: 'a' },
right: Literal { type: 'Literal', value: 1, raw: '1' } } },
今度は ExpressionStatement ですが、その中身が演算子で使っていた BinaryExpression ではなく、AssignmentExpression になっています。そこでsimplify()は次のように拡張しました。
function simplify(exp) {
// ... 省略 ...
if (exp.type === 'AssignmentExpression') {
const name = exp.left.name;
const val = simplify(exp.right);
return ['var_assign', name, val];
}
// ... 省略 ...
}
再代入部分のASTの単純化結果は、次にようになります。
[ 'var_assign', 'a', [ 'lit', 1 ] ] ]
変数の参照
変数 a の値を参照する処理のASTは、次のようになっていました。
ExpressionStatement {
type: 'ExpressionStatement',
expression: Identifier { type: 'Identifier', name: 'a' } }
expressionのタイプが Literal でなくて、Identifier になっています。これもsimplify()を拡張して対応します。
function simplify(exp) {
// ... 省略 ...
if (exp.type === 'Identifier') {
return ['var_ref', exp.name]
}
// ... 省略 ...
}
単純化結果はこちら。これで3パターンの準備が整いました。
[ 'var_ref', 'a' ]
変数処理の実行
次は変数の3種の処理(宣言、代入、参照)を実際に実行するために、evaluate()も大改造です。「RubyでつくるRuby」をお手本にして、変数を保持するためにハッシュ env を導入します。
呼び出し元でハッシュを準備、evaluate()に渡す。
let env = {};
const tree = loadAndParseSrc();
const answer = evaluate(tree, env);
println(answer);
function evaluate(tree, env) {
// ... 省略 ...
// -- 再帰的に evaluate()を読んでいる箇所では、envを引数で渡す --
if (tree[0] === '+') {
return evaluate(tree[1], env) + evaluate(tree[2], env);
}
// ... 省略 ...
}
次に、変数の宣言、代入、参照の処理を追加します。
- 宣言では、まだ変数が存在しなこと
- 代入では、変数が宣言済みなこと
- 参照では、変数が宣言済みなこと
をチェックします。
function evaluate(tree, env) {
if (tree === null) {
return null;
}
// ... 省略 ...
if (tree[0] === 'var_decl') {
// -- check NOT exist --
const name = tree[1];
if (env[name]) {
println('---ERROR: varibable ALREADY exist --');
abort();
}
env[name] = evaluate(tree[2], env);
return null;
}
if (tree[0] === 'var_assign') {
// -- check EXIST --
const name = tree[1];
if (name in env) {
env[name] = evaluate(tree[2], env);
return env[name];
}
println('---ERROR: varibable NOT declarated --');
abort();
}
if (tree[0] === 'var_ref') {
// -- check EXIST --
const name = tree[1];
if (name in env) {
return env[name];
}
println('---ERROR: varibable NOT declarated --');
abort();
}
// ... 省略 ...
}
// 処理を中断するための関数を追加
function abort() {
process.exit(1);
}
実行してみる
こちらの内容を、テスト用のソース test5.js として保存します。
// --- test variable --
let a = 1;
let b;
b = a + 2;
このファイルを引数にして、mininode_step5.js(ここまでのソースコード)を実行します。
$ node mininode_step5.js test5.js
3
変数を使った処理が実行できるようになりました。
次回
ここまでのソース
さらにデバッグ用にダミーの関数呼び出しを 'func_call' 追加して、Step 5 は終了です。全体のコードはこちら。
// -------------------------
// mininode.js - Node.js by Node.js
// Step5:
// - OK: load source file
// - OK: multi line
// - declarate variable, assign variable, refer variable
// - OK: simplify
// - OK: valuate
// - OK: println for basic function call
// -------------------------
const esprima = require("esprima");
const fs = require('fs');
// --- parser ----
function loadAndParseSrc() {
const filename = process.argv[2];
println('Loading src file:' + filename);
const src = loadSrcFile(filename);
const ast = parseSrc(src);
println('-- AST ---');
printObj(ast);
const tree = makeTree(ast);
//println('-- tree ---');
//printObj(tree);
return tree;
}
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) {
// --- handle multi lines ---
let i = 0;
let exps = [];
while (ast.body[i]) {
/* -- only ExpressionStatement --
const line = ast.body[i].expression;
const exp = simplify(line);
exps[i] = exp;
---*/
// --- for VariableDeclaration, ExpressionStatement --
const line = ast.body[i];
const exp = simplify(line);
exps[i] = exp;
i = i + 1;
}
// --- single line ---
if (exps.length === 1) {
return exps[0];
}
// --- multi lines ---
const stmts = ['stmts'].concat(exps); // <-- can bootstrup ?
return stmts;
}
function simplify(exp) {
if (exp === null) {
return null;
}
if (exp.type === 'CallExpression') {
const name = exp.callee.name;
const args = exp.arguments;
// --- only 1 arg --
const arg = simplify(args[0]);
return ['func_call', name, arg];
}
if (exp.type === 'ExpressionStatement') {
return simplify(exp.expression);
}
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];
}
println('-- ERROR: unknown kind of decralation in simplify()) ---');
printObj(exp);
abort();
}
if (exp.type === 'AssignmentExpression') {
const name = exp.left.name;
const val = simplify(exp.right);
return ['var_assign', name, val];
}
if (exp.type === 'Identifier') {
return ['var_ref', exp.name]
}
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);
abort();
}
// --- common ----
function printObj(obj) {
console.dir(obj, {depth: 10});
return null;
}
function println(str) {
console.log(str);
return null;
}
function abort() {
process.exit(1);
}
// --- evaluator ---
function evaluate(tree, env) {
if (tree === null) {
return null;
}
if (tree[0] === 'stmts') {
let i = 1;
let last;
while (tree[i]) {
last = evaluate(tree[i], env);
//println(last);
i = i + 1;
}
return last;
}
// ---- STEP 5 ---
if (tree[0] === 'func_call') {
const name = tree[1];
return println(evaluate(tree[2], env));
}
if (tree[0] === 'var_decl') {
// -- check NOT exist --
const name = tree[1];
if (name in env) {
println('---ERROR: varibable ALREADY exist --');
abort();
}
env[name] = evaluate(tree[2], env);
return null;
}
if (tree[0] === 'var_assign') {
// -- check EXIST --
const name = tree[1];
//if (env[name]) {
if (name in env) {
env[name] = evaluate(tree[2], env);
return env[name];
}
println('---ERROR: varibable NOT declarated --');
abort();
}
if (tree[0] === 'var_ref') {
// -- check EXIST --
const name = tree[1];
if (name in env) {
return env[name];
}
println('---ERROR: varibable NOT declarated --');
abort();
}
if (tree[0] === 'lit') {
return tree[1];
}
if (tree[0] === '+') {
return evaluate(tree[1], env) + evaluate(tree[2], env);
}
if (tree[0] === '-') {
return evaluate(tree[1], env) - evaluate(tree[2], env);
}
if (tree[0] === '*') {
return evaluate(tree[1], env) * evaluate(tree[2], env);
}
if (tree[0] === '/') {
return evaluate(tree[1], env) / evaluate(tree[2], env);
}
if (tree[0] === '%') {
return evaluate(tree[1], env) % evaluate(tree[2], env);
}
/*
if (tree[0] === '**') {
return evaluate(tree[1], env) ** evaluate(tree[2], env);
}
*/
if (tree[0] === '===') {
return evaluate(tree[1], env) === evaluate(tree[2], env);
}
if (tree[0] === '<') {
return evaluate(tree[1], env) < evaluate(tree[2], env);
}
if (tree[0] === '>') {
return evaluate(tree[1], env) > evaluate(tree[2], env);
}
if (tree[0] === '<=') {
return evaluate(tree[1], env) <= evaluate(tree[2], env);
}
if (tree[0] === '>=') {
return evaluate(tree[1], env) >= evaluate(tree[2], env);
}
println('-- ERROR: unknown node in evluate() ---');
printObj(tree);
abort();
}
// --------
let env = {};
const tree = loadAndParseSrc();
println('--- tree ---');
printObj(tree);
println('--- start evaluate ---');
const answer = evaluate(tree, env);
//println('--- answer ---');
println(answer);