LoginSignup
4
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-03

はじめに

「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);
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1