vm2gol v2 製作メモ(2) プログラムカウンタとVMの骨組み



第一回はさすがにあっさりすぎたのでどんどんやっていきます。

今の状態では vm = Vm.new した後に vm に対して外部から指示してあれこれやらせていて、 あんまり VM というかコンピュータっぽくない(最初に起動した後は勝手に動いてほしい)ので、それっぽくしていきます。

まずは命令をメモリに置いて、それを順次処理していくようにします。 そのためにプログラムカウンタも導入します。


まずはプログラムをメモリに直に書くようにして

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -8,7 +8,11 @@ class Vm
     @reg_b = 0
     @reg_c = 0
 
-    @mem = Array.new(8, 0) # サイズ8。0で初期化。
+    @mem = [
+      "set_reg_a", 1,
+      "set_reg_a", 0
+    ]
+  end
   end
 
   def set_mem(addr, n)
@@ -35,14 +39,3 @@ end
 vm = Vm.new
 pp vm # 初期状態
 
-vm.set_mem(0, 1)
-vm.set_mem(1, 2)
-pp vm
-
-vm.copy_mem_to_reg_a(0)
-vm.copy_mem_to_reg_b(1)
-pp vm
-
-vm.add_ab
-vm.copy_reg_c_to_mem(2)
-pp vm

@pcVm#start を追加:

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -3,6 +3,9 @@ require 'pp'
 
 class Vm
   def initialize
+    # program counter
+    @pc = 0
+
     # register
     @reg_a = 0
     @reg_b = 0
@@ -13,6 +16,24 @@ class Vm
       "set_reg_a", 0
     ]
   end
+
+  def start
+    loop do
+      # operator
+      op = @mem[@pc]
+      case op
+      when "set_reg_a"
+        n = @mem[@pc + 1]
+        @reg_a = n
+        @pc += 2
+      else
+        raise "Unknown operator (#{op})"
+      end
+
+      # 1命令実行するごとにダンプしてちょっと待つ
+      pp self
+      sleep 1
+    end
   end
 
   def set_mem(addr, n)
@@ -39,3 +60,4 @@ end
 vm = Vm.new
 pp vm # 初期状態
 
+vm.start

メモリ上では「"set_reg_a"」と「1」の2つなので、 set_reg_a した後はプログラムカウンタを2つ進めます。

これを動かすと……

$ ruby vgvm.rb 
#<Vm:0x0056111dbfd248
 @mem=["set_reg_a", 1, "set_reg_a", 0],
 @pc=0,
 @reg_a=0,
 @reg_b=0,
 @reg_c=0>
#<Vm:0x0056111dbfd248
 @mem=["set_reg_a", 1, "set_reg_a", 0],
 @pc=2,
 @reg_a=1, … ★1
 @reg_b=0,
 @reg_c=0>
#<Vm:0x0056111dbfd248
 @mem=["set_reg_a", 1, "set_reg_a", 0],
 @pc=4,
 @reg_a=0, … ★2
 @reg_b=0,
 @reg_c=0>
vgvm.rb:30:in `block in start': Unknown operator () (RuntimeError)
        from vgvm.rb:21:in `loop'
        from vgvm.rb:21:in `start'
        from vgvm.rb:67:in `<main>'

reg_a が ★1 で 1 に, ★2 で 0 になっているのが確認できますね!  プログラムカウンタも2つずつ進んでいます。


これで、

  • プログラムカウンタが指している場所から命令と引数を取ってくる
  • 命令を実行する
  • プログラムカウンタを進める
  • (くりかえし)

という VM の骨組みができました。 実装としては case による条件分岐をループで回す、という形になっていて、後はこれに肉付けしていくことになります。


最後に例外が発生して終了するのはプログラム通りの動きではありますが、 exit という命令を追加して、例外を発生させずに静かに終了するようにしておきます。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -13,7 +13,8 @@ class Vm
 
     @mem = [
       "set_reg_a", 1,
-      "set_reg_a", 0
+      "set_reg_a", 0,
+      "exit"
     ]
   end
 
@@ -22,6 +23,9 @@ class Vm
       # operator
       op = @mem[@pc]
       case op
+      when "exit"
+        $stderr.puts "exit"
+        exit
       when "set_reg_a"
         n = @mem[@pc + 1]
         set_reg_a(n)