vm2gol v2 (54) コード生成器の冗長すぎる部分を整理



気にはなっていたけど後回しにしていたアレです。

今 v3 を作っているのですが、たしか 関数呼び出しの引数や return文で任意の式を書きたくなったとかで いじっていて、コード生成器の冗長すぎる箇所をうまいこと整理できると分かりました。

良さそうだったので v2 にもフィードバックしておきます。

rename: codegen_expr() => _codegen_expr_binary()

codegen_expr() で行っているのは現状では 二項演算のみなので、名前と処理内容が乖離しています。 まずはこの乖離を解消しました。

diff は省略して変更後の _codegen_expr_binary() を貼っておきます。

def _codegen_expr_binary(fn_arg_names, lvar_names, expr)
  operator, *args = expr

  arg_l = args[0]
  arg_r = args[1]

  _codegen_expr_push(fn_arg_names, lvar_names, arg_l)
  _codegen_expr_push(fn_arg_names, lvar_names, arg_r)

  case operator
  when "+"
    _codegen_expr_add()
  when "*"
    _codegen_expr_mult()
  when "eq"
    _codegen_expr_eq()
  when "neq"
    _codegen_expr_neq()
  else
    raise not_yet_impl("operator", operator)
  end
end

reg_a への転送とスタックへの push を分離(_codegen_expr_push)

_codegen_expr_push() というメソッドがあります。

さっきリネームした _codegen_expr_binary() から呼び出されていて、 二項演算を行う前に引数を評価して結果をスタックに push する、ということをやっています。

呼び出し元はこう:

def _codegen_expr_binary(...)
  # ...

  _codegen_expr_push(fn_arg_names, lvar_names, arg_l)
  _codegen_expr_push(fn_arg_names, lvar_names, arg_r)

_codegen_expr_push() では結果を reg_a に転送するところまでで止めて、 スタックへ push するのは呼び出し側で行うようにします。

呼び出し側がこうなるように:

def _codegen_expr_binary(...)
  # ...

  _codegen_expr_push(fn_arg_names, lvar_names, arg_l)
  puts "  push reg_a"
  _codegen_expr_push(fn_arg_names, lvar_names, arg_r)
  puts "  push reg_a"
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -117,27 +117,25 @@ def codegen_while(fn_arg_names, lvar_names, rest)
 end
 
 def _codegen_expr_push(fn_arg_names, lvar_names, val)
-  push_arg =
     case val
     when Integer
-      val
+      puts "  cp #{val} reg_a"
     when String
       case
       when fn_arg_names.include?(val)
-        to_fn_arg_addr(fn_arg_names, val)
+        cp_src = to_fn_arg_addr(fn_arg_names, val)
+        puts "  cp #{cp_src} reg_a"
       when lvar_names.include?(val)
-        to_lvar_addr(lvar_names, val)
+        cp_src = to_lvar_addr(lvar_names, val)
+        puts "  cp #{cp_src} reg_a"
       else
         raise not_yet_impl("val", val)
       end
     when Array
       _codegen_expr_binary(fn_arg_names, lvar_names, val)
       #=> 結果が reg_a に入る
-      "reg_a"
     else
       raise not_yet_impl("val", val)
     end
-
-  puts "  push #{push_arg}"
 end
 
 def _codegen_expr_add

出力されるアセンブリコードが次のように変化します。 1命令だったところが2命令に増えて非効率になりますが、命令実行後の結果は変わりませんし、 コード生成処理がより良くなる方が嬉しいので気にせずやってしまいます。

-  push [bp+3]
+  cp [bp+3] reg_a
+  push reg_a

あらためて codegen_expr() を作成

def codegen_expr(fn_arg_names, lvar_names, expr)
  case expr
  when Integer
    puts "  cp #{expr} reg_a"
  when String
    case
    when fn_arg_names.include?(expr)
      cp_src = to_fn_arg_addr(fn_arg_names, expr)
      puts "  cp #{cp_src} reg_a"
    when lvar_names.include?(expr)
      cp_src = to_lvar_addr(lvar_names, expr)
      puts "  cp #{cp_src} reg_a"
    else
      raise not_yet_impl("expr", expr)
    end
  when Array
    _codegen_expr_binary(fn_arg_names, lvar_names, expr)
  else
    raise not_yet_impl("expr", expr)
  end
end

修正前は codegen_expr() が二項演算しか受け付けていなかったのが、 それに加えて、整数、関数の引数、ローカル変数も式として扱うようになりました。

たとえば

while ({式}) {
  // ...
}

という文があったとして、これまでは {式} の部分に x == 1 のような二項演算しか書けないようになっていたのが、次のような文が書けるようになるイメージです。

while (1) { ... } // 整数
while (is_foo) { ... } // 関数の引数・ローカル変数

では codegen_expr() を使って置き換えていきます。

_codegen_expr_push()

まずは _codegen_expr_push() が置き換えられます。 あらためて見ても分かるように、この段階では codegen_expr() とまったく同じ内容になっているので、置き換えて OK。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -116,28 +116,6 @@ def codegen_while(fn_arg_names, lvar_names, rest)
   puts ""
 end
 
-def _codegen_expr_push(fn_arg_names, lvar_names, val)
-    case val
-    when Integer
-      puts "  cp #{val} reg_a"
-    when String
-      case
-      when fn_arg_names.include?(val)
-        cp_src = to_fn_arg_addr(fn_arg_names, val)
-        puts "  cp #{cp_src} reg_a"
-      when lvar_names.include?(val)
-        cp_src = to_lvar_addr(lvar_names, val)
-        puts "  cp #{cp_src} reg_a"
-      else
-        raise not_yet_impl("val", val)
-      end
-    when Array
-      _codegen_expr_binary(fn_arg_names, lvar_names, val)
-    else
-      raise not_yet_impl("val", val)
-    end
-end
-
 def _codegen_expr_add
   puts "  pop reg_b"
   puts "  pop reg_a"
@@ -206,9 +184,9 @@ def _codegen_expr_binary(fn_arg_names, lvar_names, expr)
   arg_l = args[0]
   arg_r = args[1]
 
-  _codegen_expr_push(fn_arg_names, lvar_names, arg_l)
+  codegen_expr(fn_arg_names, lvar_names, arg_l)
   puts "  push reg_a"
-  _codegen_expr_push(fn_arg_names, lvar_names, arg_r)
+  codegen_expr(fn_arg_names, lvar_names, arg_r)
   puts "  push reg_a"
 
   case operator

_codegen_expr_push() はこの2箇所でしか使われていないため、用済みになりました。

_codegen_call_push_fn_arg() を置き換え

_codegen_expr_push() と同じ要領で。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -225,34 +225,14 @@ def codegen_expr(fn_arg_names, lvar_names, expr)
   end
 end
 
-def _codegen_call_push_fn_arg(fn_arg_names, lvar_names, fn_arg)
-  push_arg =
-    case fn_arg
-    when Integer
-      fn_arg
-    when String
-      case
-      when fn_arg_names.include?(fn_arg)
-        to_fn_arg_addr(fn_arg_names, fn_arg)
-      when lvar_names.include?(fn_arg)
-        to_lvar_addr(lvar_names, fn_arg)
-      else
-        raise not_yet_impl(fn_arg)
-      end
-    else
-      raise not_yet_impl(fn_arg)
-    end
-
-  puts "  push #{push_arg}"
-end
-
 def codegen_call(fn_arg_names, lvar_names, stmt_rest)
   fn_name, *fn_args = stmt_rest
 
   fn_args.reverse.each do |fn_arg|
-    _codegen_call_push_fn_arg(
+    codegen_expr(
       fn_arg_names, lvar_names, fn_arg
     )
+    puts "  push reg_a"
   end
 
   codegen_vm_comment("call  #{fn_name}")
@@ -265,9 +245,10 @@ def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
   fn_name, *fn_args = fn_temp
 
   fn_args.reverse.each do |fn_arg|
-    _codegen_call_push_fn_arg(
+    codegen_expr(
       fn_arg_names, lvar_names, fn_arg
     )
+    puts "  push reg_a"
   end
 
   codegen_vm_comment("call_set  #{fn_name}")

_codegen_call_push_fn_arg() は codegen_expr() と完全に同じではないため、 この修正によって、関数呼び出しの引数に二項演算を置けるようになります。

修正前:
call foo_func(1 + 2);
//=> コンパイルエラー

修正後:
call foo_func(1 + 2);
//=> コンパイルエラーにならない

挙動が変わってしまいますが、この変化は特に問題ないですし、記述力が高まるのでよしとします。

codegen_set() のセットする値のコード生成処理の部分

他にも置き換えられる箇所がないかと見ていきます。 codegen_set() でセットする値のコード生成を行っている部分も置き換えられそう。

var x = vram[{ローカル変数}]; のような文を書けるようにするために codegen_set() には vram[{ローカル変数}] の部分のコード生成処理があるのですが、 今の codegn_expr() ではこれはできません。

というわけで codegen_expr() で vram[{ローカル変数}] も扱えるようにします。

一応問題ないか具体例で考えてみましょうか。

// 二項演算の左右の項に vram[{ローカル変数}] が来ても問題ないか?
// ... 問題なさそう
while (vram[vi] == x) { ... }

// 関数の引数でも問題ないか?
// ... 問題なさそう
call foo_func(vram[vi] + 1);

たぶん大丈夫。

まずは codegen_expr() で vram[{ローカル変数}] も扱えるようにします。

codegen_set() からコピペして codegen_expr() を修正し、 codegen_set() からは codegen_expr() を呼び出すように修正。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -215,6 +215,15 @@ def codegen_expr(fn_arg_names, lvar_names, expr)
     when lvar_names.include?(expr)
       cp_src = to_lvar_addr(lvar_names, expr)
       puts "  cp #{cp_src} reg_a"
+    when _match_vram_ref(expr)
+      var_name = _match_vram_ref(expr)
+      case
+      when lvar_names.include?(var_name)
+        vram_addr = to_lvar_addr(lvar_names, var_name)
+        puts "  get_vram #{vram_addr} reg_a"
+      else
+        raise not_yet_impl("rest", rest)
+      end
     else
       raise not_yet_impl("expr", expr)
     end
@@ -277,39 +286,8 @@ def codegen_set(fn_arg_names, lvar_names, rest)
   dest = rest[0]
   expr = rest[1]
 
-  src_val =
-    case expr
-    when Integer
-      expr
-    when Array
-      _codegen_expr_binary(fn_arg_names, lvar_names, expr)
-      "reg_a"
-    when String
-      case
-      when fn_arg_names.include?(expr)
-        to_fn_arg_addr(fn_arg_names, expr)
-      when lvar_names.include?(expr)
-        to_lvar_addr(lvar_names, expr)
-      when _match_vram_addr(expr)
-        vram_addr = _match_vram_addr(expr)
-        puts "  get_vram #{vram_addr} reg_a"
-        "reg_a"
-      when _match_vram_ref(expr)
-        var_name = _match_vram_ref(expr)
-        case
-        when lvar_names.include?(var_name)
-          lvar_addr = to_lvar_addr(lvar_names, var_name)
-          puts "  get_vram #{ lvar_addr } reg_a"
-        else
-          raise not_yet_impl("rest", rest)
-        end
-        "reg_a"
-      else
-        raise not_yet_impl("rest", rest)
-      end
-    else
-      raise not_yet_impl("set src_val", rest)
-    end
+  codegen_expr(fn_arg_names, lvar_names, expr)
+  src_val = "reg_a"
 
   case
   when _match_vram_addr(dest)

codegen_set() の鬱陶しかった部分がすっきり!

reg_a への値のセットと push reg_a が分かれる都合で VM の修正も必要でした。 set_vram {VRAMのアドレス} reg_a のパターンです。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -448,6 +448,8 @@ class Vm
       case arg2
       when Integer
         arg2
+      when "reg_a"
+        @reg_a
       when /^ind:/
         stack_addr = calc_indirect_addr(arg2)
         @mem.stack[stack_addr]

codegen_return()

あとは codegen_return() も置き換えられます。 なんかパズルみたいで楽しい。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -312,30 +312,7 @@ end
 
 def codegen_return(lvar_names, stmt_rest)
   retval = stmt_rest[0]
-
-  case retval
-  when Integer
-    puts "  cp #{retval} reg_a"
-  when String
-    case
-    when _match_vram_ref(retval)
-      var_name = _match_vram_ref(retval)
-      case
-      when lvar_names.include?(var_name)
-        lvar_addr = to_lvar_addr(lvar_names, var_name)
-        puts "  get_vram #{lvar_addr} reg_a"
-      else
-        raise not_yet_impl("retval", retval)
-      end
-    when lvar_names.include?(retval)
-      lvar_addr = to_lvar_addr(lvar_names, retval)
-      puts "  cp #{lvar_addr} reg_a"
-    else
-      raise not_yet_impl("retval", retval)
-    end
-  else
-    raise not_yet_impl("retval", retval)
-  end
+  codegen_expr([], lvar_names, retval);
 end
 
 def codegen_vm_comment(comment)

これもすっきり。うーむ、もっと早くやってればよかった……。

while, case の条件式

while (ここ) { ... }

case {
  (ここ) { ... }
}

これらは現状では二項演算のみ受け付けるようになっていますが、 ここもついでに codegen_expr() に置き換えてしまいます。

while (do_break) { ... }

みたいなことができると嬉しいと思いますし、 二項演算だけに限定する理由もあんまりないと思いますし。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -48,7 +48,7 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
     when "eq"
       # 式の結果が reg_a に入る
       puts "  # -->> expr"
-      _codegen_expr_binary(fn_arg_names, lvar_names, cond)
+      codegen_expr(fn_arg_names, lvar_names, cond)
       puts "  # <<-- expr"
 
       # 式の結果と比較するための値を reg_b に入れる
@@ -94,7 +94,7 @@ def codegen_while(fn_arg_names, lvar_names, rest)
   puts "label #{label_begin}"
 
   # 条件の評価 ... 結果が reg_a に入る
-  _codegen_expr_binary(fn_arg_names, lvar_names, cond_expr)
+  codegen_expr(fn_arg_names, lvar_names, cond_expr)
   # 比較対象の値(真)をセット
   puts "  set_reg_b 1"
   puts "  compare"

ここまでの修正により、 _codegen_expr_binary() の呼び出し元が codegen_expr() だけになりました。

codegen_case() の不要な分岐を削除

codegen_expr() でさまざまな式を統一的に扱えるようになったため、演算子を見て振り分ける処理が不要になりました。 eq 以外をあえてエラーにする理由も特にないですし、行数も減ってすっきりさせられるので削除します。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -40,12 +40,9 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
   when_blocks.each do |when_block|
     when_idx += 1
     cond, *rest = when_block
-    cond_head, *cond_rest = cond
 
     puts "  # when_#{label_id}_#{when_idx}: #{cond.inspect}"
 
-    case cond_head
-    when "eq"
       # 式の結果が reg_a に入る
       puts "  # -->> expr"
       codegen_expr(fn_arg_names, lvar_names, cond)
@@ -67,10 +64,6 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
 
       # 偽の場合ここにジャンプ
       puts "label #{label_end_when_head}_#{when_idx}"
-
-    else
-      raise not_yet_impl("cond_head", cond_head)
-    end
   end
 
   puts "label #{label_end}"

コードの量はどのくらい減ったか

[before]
   41 common.rb
   65 vgasm.rb
  478 vgcg.rb
   58 vglexer.rb
  454 vgparser.rb
  512 vgvm.rb
 1608 合計

[after]
   64 vgasm.rb
  408 vgcg.rb
   58 vglexer.rb
  454 vgparser.rb
  514 vgvm.rb
   41 common.rb
 1539 合計

70行近く減らせました! 🙌

(まあ、元が冗長すぎたんですけどね)