Node.jsでつくるNode.js - Step 5: 変数を使う

# はじめに

「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」([ラムダノート](https://www.lambdanote.com/products/ruby-ruby), [Amazon](https://www.amazon.co.jp/dp/4908686017)) と [PythonでつくるPython](http://akimacho.hatenablog.com/entry/2018/05/22/220109)に影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。

[前回](https://qiita.com/massie_g/items/40591822228d7e166b63)は四則演算（+, -, *, /)で電卓を作りました。今回は「RubyでつくるRuby」の第5章と同じく、 複数行のソースへの対応と、変数を利用するところまでを目指します。

* [もくじ](https://qiita.com/massie_g/items/3ee11c105b4458686bc1)

# ソースファイルの読み込み

loadSrcFile()では引数で渡されたファイル名のソースを、fs.readFileSync()で読み込みます。

```js

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を調べて見ます。

```js
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()関数を拡張しました。

```js
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()も、同様に拡張します。

```js
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が返ってきます。

```js
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()も合わせて修正が必要です。

```js
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()は次のように拡張しました。

```js
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()を拡張して対応します。

```js
function simplify(exp) {
  // ... 省略 ...

  if (exp.type === 'Identifier') {
    return ['var_ref', exp.name]
  }

  // ... 省略 ...
}
```

単純化結果はこちら。これで3パターンの準備が整いました。

```
  [ 'var_ref', 'a' ]
```

## 変数処理の実行

次は変数の3種の処理（宣言、代入、参照）を実際に実行するために、evaluate()も大改造です。「RubyでつくるRuby」をお手本にして、変数を保持するためにハッシュ env を導入します。

呼び出し元でハッシュを準備、evaluate()に渡す。

```js
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);
  }

  // ... 省略 ...
}

```

次に、変数の宣言、代入、参照の処理を追加します。

* 宣言では、まだ変数が存在しなこと
* 代入では、変数が宣言済みなこと
* 参照では、変数が宣言済みなこと

をチェックします。

```js
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 として保存します。

```js
// --- test variable --
let a = 1;
let b;
b = a + 2;
```

このファイルを引数にして、mininode_step5.js（ここまでのソースコード）を実行します。

```
$ node mininode_step5.js test5.js
3
```

変数を使った処理が実行できるようになりました。

# 次回

* [Step 6: 条件分岐と繰り返し](https://qiita.com/massie_g/items/f801692fb9b07d007285)

# ここまでのソース

さらにデバッグ用にダミーの関数呼び出しを 'func_call' 追加して、Step 5 は終了です。全体のコードはこちら。

```js
// -------------------------
// 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);
```