vm2gol v2 (46) リファクタリング(主にコード生成器)



コード生成器で気になっていた部分を修正しました。

codegen_stmts() まわりの整理

修正前の状況を見てみましょう。 呼び出しの関係が次のようになっています。

呼び出す側    => 呼び出される側
codegen       => codegen_stmts
codegen_case  => codegen_stmts
codegen_while => codegen_stmts

まず codegen_stmtscodegen_top_stmts にリネーム。

リネーム後はこうなります:

codegen       => codegen_top_stmts
codegen_case  => codegen_top_stmts
codegen_while => codegen_top_stmts

codegen => codegen_top_stmts はいいとして、 codegen_casecodegen_while から codegen_top_smtms を呼んでいるのは変ですね。 これだと、たとえば case 文や while 文の中で関数が定義できてしまいます (できるけどそういうコードを書かないようにしていた、という状態)。

リネームによっておかしなところが目立つようになりました。 おかしいので是正していきます。

まずは codegen_func_def 内で関数の処理本体のコード生成を行っていた部分を 改めて codegen_stmts としてメソッド抽出します。 トップレベル以外の場所にある文の連なりの処理です。

(追記 2020-08-25) テストが通っていたので雑にやってしまいましたが、 lvar_names の扱いがおかしいことに気付きました。あとで修正します……。

def codegen_stmts(fn_arg_names, lvar_names, stmts)
  alines = []

  stmts.each do |stmt|
    stmt_head, *stmt_rest = stmt

    case stmt_head
    when "call"
      alines += codegen_call(fn_arg_names, lvar_names, stmt_rest)
    when "call_set"
      alines += codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
    when "var"
      lvar_names << stmt_rest[0]
      alines << "  sub_sp 1"
      if stmt_rest.size == 2
        alines += codegen_set(fn_arg_names, lvar_names, stmt_rest)
      end
    when "set"
      alines += codegen_set(fn_arg_names, lvar_names, stmt_rest)
    when "eq"
      alines += codegen_exp(fn_arg_names, lvar_names, stmt)
    when "return"
      alines += codegen_return(lvar_names, stmt_rest)
    when "case"
      alines += codegen_case(fn_arg_names, lvar_names, stmt_rest)
    when "while"
      alines += codegen_while(fn_arg_names, lvar_names, stmt_rest)
    when "_cmt"
      alines += codegen_comment(stmt_rest[0])
    else
      raise not_yet_impl("stmt_head", stmt_head)
    end
  end

  alines
end

こうなりました:

codegen          => codegen_top_stmts
codegen_case     => codegen_top_stmts
codegen_while    => codegen_top_stmts
codegen_func_def => codegen_stmts

あとは codegen_casecodegen_while から codegen_top_smtms ではなく codegen_stmts を呼ぶようにすると、こうなります:

codegen          => codegen_top_stmts
codegen_case     => codegen_stmts
codegen_while    => codegen_stmts
codegen_func_def => codegen_stmts

これで意味的に変だったところがまともになりました。 テストが壊れていないので動作の方も問題なさそうです。 たぶん大丈夫。 ライフゲーム動いてるから。

これで codegen_top_stmts の呼び出し元が codegen だけになりました。 トップレベルに書けるのは今のところ関数定義とコメントだけでよいので、 それ以外の分岐は消してしまいます(上述の修正で不要になったので消してOK)。 これにより引数 fn_arg_names, lvar_names も不要になったのでこれも除去。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -428,7 +428,7 @@ def codegen_func_def(rest)
   alines
 end
 
-def codegen_top_stmts(fn_arg_names, lvar_names, rest)
+def codegen_top_stmts(rest)
   alines = []
 
   rest.each do |stmt|
@@ -436,16 +436,6 @@ def codegen_top_stmts(fn_arg_names, lvar_names, rest)
     case stmt_head
     when "func"
       alines += codegen_func_def(stmt_rest)
-    when "call"
-      alines += codegen_call(fn_arg_names, lvar_names, stmt_rest)
-    when "call_set"
-      alines += codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
-    when "set"
-      alines += codegen_set(fn_arg_names, lvar_names, stmt_rest)
-    when "case"
-      alines += codegen_case(fn_arg_names, lvar_names, stmt_rest)
-    when "while"
-      alines += codegen_while(fn_arg_names, lvar_names, stmt_rest)
     when "_cmt"
       alines += codegen_comment(stmt_rest[0])
     else
@@ -464,7 +454,7 @@ def codegen(tree)
 
   head, *rest = tree
   # assert head == "stmts"
-  alines += codegen_top_stmts([], [], rest)
+  alines += codegen_top_stmts(rest)
 
   alines
 end

第38回codegen_stmts()codegen_func_def() の共通化 について触れたときは、なんか変だなとは思っていたもののこの形がまだ見えていませんでした。 慌てていじらずに寝かせておいてよかった気がします。

codegen_exp()

codegen_exp() が長くて(106行……)ちょっと辛いのと、 二項演算の左右の項をスタックに push する処理が重複していたので、 メソッド抽出して整理して、最終的に次のようになりました。すっきり。

def codegen_exp(fn_arg_names, lvar_names, exp)
  alines = []
  operator, *args = exp

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

  alines += _codegen_exp_push(fn_arg_names, lvar_names, arg_l)
  alines += _codegen_exp_push(fn_arg_names, lvar_names, arg_r)

  case operator
  when "+"
    alines += _codegen_exp_add()
  when "*"
    alines += _codegen_exp_mult()
  when "eq"
    alines += _codegen_exp_eq()
  when "neq"
    alines += _codegen_exp_neq()
  else
    raise not_yet_impl("operator", operator)
  end

  alines
end

その他

  • codegen_set(): caseの分岐を他の箇所に揃えた
    • 他の箇所に揃えて、まず型で分岐させる書き方に
  • Vm#copy: 記述位置の移動のみ
  • vgparser.rb: tokenize() の記述位置の移動のみ
    • パーサのコードを調べてるとき、末尾に tokenize() があると邪魔くさかったので