vm2gol v2 (53) 間接メモリ参照のフォーマットの改良 / alinesへの蓄積をやめる



間接メモリ参照のフォーマットの改良

機械語での間接メモリ参照はこれまでアセンブリでとまったく同じ [bp-2], [bp+3] のようなフォーマットにしていて、 VM では /^\[bp-(\d+)\]$/ のような正規表現を使うことで 間接メモリ参照であることを判定したり bp からの相対位置(displacement)を取り出したりしていました。

しかし、この VM は 一応なんらかの CPU のシミュレータのつもりですから (オレオレのかなりいい加減なものとはいえ……) 、 正規表現とか高級なことはなるべくさせないようにしたい。

そこで今回、以下のようにちょっとだけ機械が読みやすそうな表現に変えました。

(before)
[bp-2]
[bp+3]

(after)
ind:bp:-2
ind:bp:3

見ての通りです。 ind は indirection の略。

この変換はアセンブラで行います。

--- a/vgasm.rb
+++ b/vgasm.rb
@@ -31,6 +31,15 @@ def create_label_addr_map(alines)
   map
 end
 
+def to_machine_code_operand(arg)
+  case arg
+  when /^\[bp\-(\d+)\]$/ then "ind:bp:-#{$1}"
+  when /^\[bp\+(\d+)\]$/ then "ind:bp:#{$1}"
+  when /^-?\d+$/         then arg.to_i
+  else                        arg
+  end
+end
+
 src = File.read(ARGV[0])
 alines = parse(src)
 
@@ -49,9 +58,7 @@ alines.each do |aline|
     label_name = rest[0]
     insn << label_addr_map[label_name]
   else
-    insn += rest.map {|arg|
-      (/^-?\d+$/ =~ arg) ? arg.to_i : arg
-    }
+    insn += rest.map {|arg| to_machine_code_operand(arg) }
   end
 
   puts JSON.generate(insn)

影が薄かったアセンブラちゃんの仕事を増やせてよかったですね(?)。 base の部分には bp しか来ないので決め打ちにしています。

VM 側の修正はこんな感じ:

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -268,6 +268,20 @@ class Vm
     DUMP
   end
 
+  def calc_indirect_addr(str)
+    _, base_str, disp_str = str.split(":")
+
+    base =
+      case base_str
+      when "bp"
+        @bp
+      else
+        raise not_yet_impl("base_str", base_str)
+      end
+
+    base + disp_str.to_i
+  end
+
   def add_ab
     @reg_a = @reg_a + @reg_b
   end

@@ -283,11 +297,8 @@ class Vm
       case val
       when Integer
         val
-      when /^\[bp-(\d+)\]$/
-        stack_addr = @bp - $1.to_i
-        @mem.stack[stack_addr]
-      when /^\[bp\+(\d+)\]$/
-        stack_addr = @bp + $1.to_i
+      when /^ind:/
+        stack_addr = calc_indirect_addr(val)
         @mem.stack[stack_addr]
       else
         raise not_yet_impl("val", val)

先頭だけ見ればよいので ^ind: と簡素な正規表現で判定できるようになり、 calc_indirect_addr() でも単に split(":") で済むようになりました。

また、 displacement の正負も統一的に表現できるようになったこともあり、今回あわせて分岐も減らしています。

case 式の都合に合わせるため正規表現は残していますが、 なくそうと思えばいつでもなくせる状態になってます (たとえば 〜.start_with?("ind:") に書き直すとかで) ので、まあこれでええでしょということにします。

(追記 2021-02-06) アセンブリでのフォーマットも変更

パースがしやすいのと、 数値の正負によって挙動を切り替える煩雑さがなくなってすっきりするため、 機械語と同様にアセンブリでのフォーマットもコロンで区切る形に変えることにしました。

素人の浅知恵かもしれませんが、これではダメだということになったらまたそのとき考えましょう。

(before)
[bp-2]
[bp+3]

(after)
[bp:-2]
[bp:3]

これにより、機械語への変換処理も次のように簡素に。

# vgasm.rb

def to_machine_code_operand(arg)
  case arg
  when /^\[(.+)\]$/ then "ind:#{$1}"
  when /^-?\d+$/    then arg.to_i
  else                   arg
  end
end

コード生成の簡素化(alines への蓄積をやめる)

 def codegen(tree)
-  alines = []
-
-  alines << "  call main"
-  alines << "  exit"
+  puts "  call main"
+  puts "  exit"
 
   head, *rest = tree
   # assert head == "stmts"
-  alines += codegen_top_stmts(rest)
+  codegen_top_stmts(rest)
-
-  alines
 end

各メソッドの先頭で配列 alines を用意していた部分と alines を返却していた部分がなくなり、 アセンブリコードを alines に蓄積するのをやめてその場で出力するようにしました。

副作用がない方がテストとかしやすいかなと思って修正前のような書き方をしていたのですが、 コード生成処理に関しては結局メソッド単位のテストは書いていませんし、 その場で出力する方式に変えても特に問題ない ( 第49回 で問題ないようにした ) ので、簡単で分かりやすい書き方に変えることにしました。

移植版の方で何度か試して悪くなさそうだったので Ruby版にフィードバックした形。

その他の修正

  • rbenv local 2.6.6
    • Ruby 3.0 がリリースされたので2つ前のバージョンということで 2.6 に上げました (これまでは 2.5 で作ってました)
  • Ruby の警告に従って修正: IO#lines => #each_line
    • IO#lines is deprecated; use #each_line instead
  • vgparser.rb: インデントの修正のみ
    • 前回の後始末
  • dump_exe.rb を削除
    • 第8回 のときに書いたものだが、結局その後ほとんど使わなかったのと、 今回の修正で不要になったので。