このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

JavaScriptで仮想機械の勉強をしましょう

事情があって、コンピュータのハードウェアメカニズムをごく簡単に説明したいのです。本物のCPUは難しくて簡単な説明にはならないし、そもそも僕は本物がどうなっているかよく知りません。

そこで、オモチャの仮想機械を作ることにしました。加減乗除だけができるスタックマシンです。仮想機械の実装はJavaScriptユーザーインターフェースはHTMLです。こういうとき、JavaScript + HTML はお手軽でほんとに便利です。

Web上で試せるURL

初等算術計算仮想機械の実装

仮想機械を実装するソースコードは次のようです。インストラクション(機械語命令)の説明はすぐ後でします。

/* rac0vm.js */
/*
 * Rudimentary Arithmetic Calculator Level-0
 */

var C  = 0; // 定数値 (Constant)
var OP = 1; // 演算 (Operation)

var code;   // 現在実行中のコード
var pc;     // プログラムカウンタ
var stack;  // 計算用スタック

var result; // 最終結果

var debug = true; // デバッグフラグ

function setup(c) {
  code = c;
  pc = 0;
  stack = [];
}

function exec() {
  while (true) {
    if (pc >= code.length) break;
    var inst = code[pc];
    if (debug) {
      var mess = instToString(inst) + "\n" + stackToString();
      alert(mess);
    }
    if (inst[0] === C) {
      stack.push(inst[1]);
    } else if (inst[0] == OP){
      doOperation(inst[1]);
    } else {
      throw "illegal instruction";
    }
    pc++;
  }
  result = stack.pop();
}

function doOperation(opcode) {
  // スタックから引数を取り出すときは逆順
  var arg2 = stack.pop();
  var arg1 = stack.pop();
  // opcode により分岐実行
  switch (opcode) {
  case "+":
    stack.push(arg1 + arg2);
    break;
  case "-":
    stack.push(arg1 - arg2);
    break;
  case "*":
    stack.push(arg1 * arg2);
    break;
  case "/":
    stack.push(Math.floor(arg1 / arg2));
    break;
  case "%":
    stack.push(arg1 % arg2);
    break;
  default:
    throw "illegal opcode";
  }
}

// 以下、デバッグメッセージ用

function instToString(inst) {
  var s;
  switch (inst[0]) {
  case C:
    s = "Const:";
    break;
  case OP:
    s = "Opera:";
    break;
  default:
    return "Illegal instruction!";
  }
  s += inst[1];
  return s;
}

function stackToString() {
  var s = "Stack|";
  for (var i = 0; i < stack.length; i++) {
    s += stack[i];
    s += "|";
  }
  return s;
}

stackToString() でforループ回しているところは、stack.join("|") ってやりゃ良かった、と今思ったがまーいいや。

インストラクション(機械語命令)

1つのインストラクションは、種別を表す接頭辞とデータの組です。種別は定数(constant)C、または演算(operation)OPのどちらかです。定数は整数に限ります(実際は、JavaScript浮動小数点も使えてしまいますが)。演算は算数の四則演算、+, -, *, /, % です。% は割り算の余りを求める演算。

インストラクションの記述形式は [種別, データ] の形:

  • 定数 1 : [C, 1]
  • 定数 2 : [C, 2]
  • 演算 + : [OP, "+"]

インストラクションをカンマで区切って並べたものがプログラムです。[C, 1], [C, 2], [OP, "+"] はプログラムですね。カンマを忘れがちですから注意してください。

定数はスタックにそのまま置かれ、演算はスタック上の値を引数として実行されます。したがって、[C, 1], [C, 2], [OP, "+"] は 1 + 2 を計算します。http://www.chimaira.org/tools/rac0.html のテキストエリアに機械語コードを入れて実行してみてください。デバッグ用アラートには、これから実行するインストラクションと、その時(インストラクション実行直前)のスタック状態が表示されます。最終結果はHTMLページ内に書き込まれます。

逆ポーランド記法

スタック計算機の機械語コードは、だいたい逆ポーランド記法になります。逆ポーランド記法とは、演算の引数(被演算数)を先に書き、演算子を後に置く書き方です。例えば、
2 + 3 は 2 3 + となります。以下では、引数/演算子の区切りにカンマを入れることにします。2, 3, + のようになります。このほうが、今話題にしている仮想機械RAC0機械語コードに近いですから。

逆ポーランド記法では、括弧を使う必要がなく、並べたまんまに計算が進行します。スタックマシンとたいへん相性がいいのです。いくつかの例を見れば、逆ポーランド記法をすぐに理解できると思いますよ。以下に、普通の算術式、逆ポーランド記法RAC0機械語コードを並べた例を示します。

  • 2*3 + 5
  • 2, 3, *, 5, +
  • [C, 2], [C, 3], [OP, "*"], [C, 5], [OP, "+"]
  • 2*(3 + 5)
  • 2, 3, 5, +, *
  • [C, 2], [C, 3], [C, 5], [OP, "+"], [OP, "*"]
  • 2*3 - 10%3
  • 2, 3, *, 10, 3, %, -
  • [C, 2], [C, 3], [OP, "*"], [C, 10], [C, 3], [OP, "%"], [OP, "-"]

こんな簡単なオモチャ仮想機械でも、いじってみると、「計算機による計算て、こんなふうに進むのか」という感じを味わえるのではないかと思います。

RAC*

Rudimentary Arithmetic Calculator Leval-0 と名付けたので、Level-1, Level-2 などがあるかもしれません。条件分岐や関数の定義/呼び出しができる機構をサポートするかもね。