vm2gol v2 製作メモ(12) リファクタリング / cp


道具が用意できたので引数渡しをやりたい! のですが、今回はリファクタリング回にします。

前回やっつけで追加した copy_sp_to_bpcopy_bp_to_sp を整理して、 せっかくのリファクタリング回なので他の部分もあわせて整理しておきます。


まず、

case arg
when "bp"
  @bp
else
  raise "push: not yet implemented (#{arg})"
end

のように、 case 式で分岐させ、 else で「その値に関する処理はまだ実装してないよ」と例外を投げている箇所が ありますが、以後も出てくるので共通化しておきます。

これは別ファイル common.rb に抽出します。

# common.rb

def p_e(*args)
  args.each{ |arg| $stderr.puts arg.inspect }
end

def pp_e(*args)
  args.each{ |arg| $stderr.puts arg.pretty_inspect }
end

def not_yet_impl(*args)
  "Not yet implemented" +
    args
    .map{ |arg| " (#{ arg.inspect })" }
    .join("")
end

ついでにデバッグ用の p_e, pp_e というべんりメソッドも足しておきました。

not_yet_impl() の中で例外を投げるとこまでやってしまおうかとも 考えましたが、 なるべく単機能(例外用のメッセージを組み立てるだけ)になっている方がよいかな? とか、 呼び出し元を読んだときにraise していることが分かった方が明示的でよいかな? などと考えて控えめにしておきました。


呼び出し元を修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -2,6 +2,8 @@
 require 'pp'
 require 'yaml'
 
+require './common'
+
 module TermColor
   RESET  = "\e[m"
   RED    = "\e[0;31m"
@@ -174,7 +176,7 @@ class Vm
           when "bp"
             @bp
           else
-            raise "push: not yet implemented (#{arg})"
+            raise not_yet_impl("push", arg)
           end
         @sp -= 1
         @mem.stack[@sp] = val_to_push
@@ -185,7 +187,7 @@ class Vm
         when "bp"
           @bp = @mem.stack[@sp]
         else
-          raise "pop: not yet implemented (#{arg})"
+          raise not_yet_impl("pop", arg)
         end
         @sp += 1
         @pc += 2

次に、値のコピーまわりを整理します。

前回はとにかく早く動かしたかったので bp, sp 間のコピー用に専用の命令を追加してしまいましたが、 コピー用の命令は今後もいろいろ使うので、 この段階で汎用化しておきます。


一般的な(?)アセンブリ言語では こういうとき mov(move)という命令を使うようです。 ただし、自分が見聞きした範囲でも

  • move(移動)じゃなくてコピーでは?
  • オペランドの順番(コピー元、先)が Intel 記法と AT&T 記法で違っていて混乱する

という意見がちらほらあるようで(どのくらいポピュラーかは分かりませんが……)、個人的にも微妙に感じたので、 それならいっそのこと cp (copy) にしてはどうか、となりました。

オペランドの順番問題については、 これは UNIX の cp コマンドと同じインターフェイスにしてみます。 つまり、コピー元、コピー先、の順番です。 分かりやすいですね? 自分にとって分かりやすいのでこれでいいということにします。 よく知られた既存のインターフェイスに合わせることで覚える手間をなくそう、余分な負荷を削ろうという工夫のつもりでもあります。

どうせ自分向けに作っているオレオレのものなので、 こういうのは「自分が覚えやすいかどうか」を基準にして決めてしまえばよい、と割り切っています(jump_eq のような他の命令もそういう基準で適当に決めています)。


適当に決めたので適当に修正しましょう。

まず cp 命令を追加。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -143,6 +143,12 @@ class Vm
       when "copy_sp_to_bp"
         @bp = @sp
         @pc += 1
+      when "cp"
+        copy(
+          @mem.main[@pc + 1],
+          @mem.main[@pc + 2]
+        )
+        @pc += 3
       when "add_ab"
         add_ab()
         @pc += 1
@@ -200,8 +206,31 @@ class Vm
     end
   end
 
+  def copy(arg1, arg2)
+    src_val =
+      case arg1
+      when "sp"
+        @sp
+      when "bp"
+        @bp
+      else
+        raise not_yet_impl("copy src", arg1)
+      end
+
+    case arg2
+    when "bp"
+      @bp = src_val
+    when "sp"
+      @sp = src_val
+    else
+      raise not_yet_impl("copy dest", arg2)
+    end
+  end
+
   def self.num_args_for(operator)
     case operator
+    when "cp"
+      2
     when "set_reg_a", "set_reg_b", "label", "call", "push", "pop"
       1
     when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"

copy_bp_to_spcopy_sp_to_bp はもう使わないので消しておきます。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -137,12 +137,6 @@ class Vm
         n = @mem.main[@pc + 1]
         @reg_c = n
         @pc += 2
-      when "copy_bp_to_sp"
-        @sp = @bp
-        @pc += 1
-      when "copy_sp_to_bp"
-        @bp = @sp
-        @pc += 1
       when "cp"
         copy(
           @mem.main[@pc + 1],
@@ -233,7 +227,7 @@ class Vm
       2
     when "set_reg_a", "set_reg_b", "label", "call", "push", "pop"
       1
-    when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"
+    when "ret", "exit"
       0
     else
       raise "Invalid operator (#{operator})"

前回のアセンブリコードを修正。

--- a/11_bp_push_pop.vga.txt
+++ b/11_bp_push_pop.vga.txt
@@ -6,13 +6,13 @@
 label sub
   # 前処理
   push bp
-  copy_sp_to_bp
+  cp sp bp
 
   # サブルーチン本体の処理
   set_reg_a 2
 
   # 後片付け
-  copy_bp_to_sp
+  cp bp sp
   pop bp
 
   ret

修正後のアセンブリコードが

cp bp sp

となって、アセンブリのコードなんだかシェルスクリプトなんだか よく分からない感じになって楽しくなりましたね(?)。


そういえば特に何も言わずにやってますが、 VM の命令名とアセンブリニーモニックが一致しているので こういう場合もアセンブラは修正なしです。

つくづく影の薄いアセンブラ……。


あと気になっていたのが、命令ごとのオペランド(引数)の数に関する知識が vgvm.rb のコード中に散らばっていることです。

せっかく Vm.num_args_for がすでにあるので、これを使いまわしましょう。

(今のうちに対処しておかないとこの先辛いってわけではなく、 とりあえず DRY にしときましょうか、という程度のノリです。)

@@ -118,6 +118,9 @@ class Vm
     loop do
       # operator
       op = @mem.main[@pc]
+
+      pc_delta = 1 + Vm.num_args_for(op)
+
       case op
       when "exit"
         $stderr.puts "exit"
@@ -125,32 +128,32 @@ class Vm
       when "set_reg_a"
         n = @mem.main[@pc + 1]
         set_reg_a(n)
-        @pc += 2
+        @pc += pc_delta
       when "set_reg_b"
         n = @mem.main[@pc + 1]
         set_reg_b(n)
-        @pc += 2
+        @pc += pc_delta
       when "set_reg_c"
         n = @mem.main[@pc + 1]
         set_reg_c(n)

以下同様

修正した 11_bp_push_pop.vga.txt を実行してみます。

$ ./run.sh 11_bp_push_pop.vga.txt 
(略)
================================
reg_a(2) reg_b(3) reg_c(0) zf(0)
---- memory (main) ----
      00   ["set_reg_a", 1]
      02   ["call", 9]
      04   ["set_reg_b", 3]
pc => 06   ["exit"]
      07 ["label", "sub"]
      09   ["push", "bp"]
      11   ["cp", "sp", "bp"]
      14   ["set_reg_a", 2]
      16   ["cp", "bp", "sp"]
      19   ["pop", "bp"]
      21   ["ret"]
---- memory (stack) ----
         0 0
         1 3
         2 4
sp bp => 3 0

exit
$

問題ないですね! 次回は引数渡しに戻ります!!!!