vm2gol v2 (48) 変数宣言のコード生成処理の改善など



Java版 を書いているときに codegen_stmts() の微妙なところに気付いてしまいました。

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"
      # ...

メソッドの引数 lvar_names を変更しています。 なんでこうなってるかというと、 以前リファクタリングしたとき(第46回) に雑にやってしまっていたからですね……。


Ruby の場合は参照渡し的な挙動になるので、 うまく動いてテストも通っていて(だからすぐ気づかなかったのですが)、特別まずいというわけでもないと思います。

def foo(arg_xs)
  arg_xs << 2
end

xs = [1]
foo(xs)
foo(xs)
p xs #=> [1, 2, 2]

とはいえ、呼び出し先で変更すると挙動が把握しにくかったりしますし、もうちょっといい感じにできないかなと。


というわけで修正します。

まず、codegen_stmts() のループ内で行っている1つの文の処理を codegen_stmt() に抽出。これは単純なリファクタリング

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -415,38 +415,44 @@ def codegen_comment(comment)
   ]
 end
 
+def codegen_stmt(fn_arg_names, lvar_names, stmt)
+  stmt_head, *stmt_rest = stmt
+
+  case stmt_head
+  when "call"
+    codegen_call(fn_arg_names, lvar_names, stmt_rest)
+  when "call_set"
+    codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
+  when "var"
+    alines = []
+    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
+    alines
+  when "set"
+    codegen_set(fn_arg_names, lvar_names, stmt_rest)
+  # when "eq"
+  #   alines += codegen_exp(fn_arg_names, lvar_names, stmt)
+  when "return"
+    codegen_return(lvar_names, stmt_rest)
+  when "case"
+    codegen_case(fn_arg_names, lvar_names, stmt_rest)
+  when "while"
+    codegen_while(fn_arg_names, lvar_names, stmt_rest)
+  when "_cmt"
+    codegen_comment(stmt_rest[0])
+  else
+    raise not_yet_impl("stmt_head", stmt_head)
+  end
+end
+
 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
+    alines += codegen_stmt(fn_arg_names, lvar_names, stmt)
   end
 
   alines

次に、codegen_stmt() で変数宣言を処理するのをやめます。

それだけだと変数宣言できなくなってしまうので、ではどうするかというと、 変数宣言だけ codegen_func_def() で処理してしまうことにしました。 これで変数名を lvar_names に追加する処理が codegen_func_def() に戻りました。

これが今回の修正のメインとなるコミットです。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -423,14 +423,6 @@ def codegen_stmt(fn_arg_names, lvar_names, stmt)
     codegen_call(fn_arg_names, lvar_names, stmt_rest)
   when "call_set"
     codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
-  when "var"
-    alines = []
-    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
-    alines
   when "set"
     codegen_set(fn_arg_names, lvar_names, stmt_rest)
   # when "eq"
@@ -475,7 +467,18 @@ def codegen_func_def(rest)
 
   lvar_names = []
 
-  alines += codegen_stmts(fn_arg_names, lvar_names, body)
+  body.each do |stmt|
+    if stmt[0] == "var"
+      _, *stmt_rest = stmt
+      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
+    else
+      alines += codegen_stmt(fn_arg_names, lvar_names, stmt)
+    end
+  end
 
   alines << ""
   alines << "  cp bp sp"

codegen_stmt()codegen_while()codegen_case() からも呼ばれているため、 この変更により while 文や case 文の中で変数宣言できなくなってしまいます。 が、その点は特に困らないからいいだろうという判断にしました。

昔のC言語だと関数の先頭でしかローカル変数宣言できませんでしたし、 大丈夫なんじゃないかな……。


あとは変数宣言のコード生成処理を codegen_var() に抽出して、 関数の body を処理する部分が次のようになりました。

  body.each do |stmt|
    if stmt[0] == "var"
      _, *stmt_rest = stmt
      lvar_names << stmt_rest[0]
      alines += codegen_var(fn_arg_names, lvar_names, stmt_rest)
    else
      alines += codegen_stmt(fn_arg_names, lvar_names, stmt)
    end
  end

その他

移植のときに気付いたもののフィードバックなど。

  • codegen_while() などで出てくるラベル名を変数化して DRY に
  • 式関連の変数名などがパーサでは expr、コード生成器では exp となっていて 不揃いだったので expr に統一
  • comment → vm_comment にリネーム
  • Token#type の値を変更
    • :reserved:kw など