vm2gol v2 製作メモ(21) case文


関数まわりが整備できたので、次は条件分岐(if文)やりましょうか。

えーと、こんな感じで……

["if"
, ["eq", 33, 31]
, [
    ["set_reg_a", 11]
  ]
, [
    ["set_reg_a", 22]
  ]
]

こんな感じで作っていましたが、この後すぐに 「case 文で代用できるから if文はなくても良かったな…」 となって if文のコード生成をなくしてしまいました。

というわけで if文はなかったことにして case文をやります。


こっちが case文の文法です:

// 21_case.vgt.json

["case"

, [ ["eq", 44, 45]    // 条件1
  , ["set_reg_a", 11] // 条件1 が真だった場合に実行
  ]

, [ ["eq", 55, 56]    // 条件2
  , ["set_reg_a", 22] // 条件2 が真だった場合に実行
  ]

  // else節に相当
, [ ["eq", 0, 0]
  , ["set_reg_a", 33]
  ]

]

Lisp の cond ですね。

else 節部分の ["eq", 0, 0] 部分がちょっと不格好ですが、 今の段階でもこうすれば動作としては期待するものになるので、 いったんこれで。 まずは今使えるものでなんとかします (結局最後までこのままなんですが)。

C言語Java の switch文とは違ってフォールスルーはせず、したがって break もなし。

上のコードだと条件1 も条件2 も真にならず else 節が実行されますが、動作確認するときは適当に 条件1 が真になるように書き換えたり、 条件2 が真になるように書き換えたりしてやっていきます (または、そういうパターンを別ファイルで用意してもよいと思います) 。


まずは期待されるアセンブリコードを考えます。 ちょっと長い。

(※ 最終的にはラベルの部分が少し変わります。後述。)

  # 条件1 を評価
  set_reg_a 44
  set_reg_b 45
  compare
  jump_eq when_0 # 条件1 の body にジャンプ

  # 条件2 を評価
  set_reg_a 55
  set_reg_b 56
  compare
  jump_eq when_1 # 条件2 の body にジャンプ

  # 条件3 を評価
  set_reg_a 0
  set_reg_b 0
  compare
  jump_eq when_2 # 条件2 の body にジャンプ

  jump end_case # すべての条件が偽だった場合

# 条件1 の body
label when_0
  set_reg_a 11
  jump end_case

# 条件2 の body
label when_1
  set_reg_a 22
  jump end_case

# 条件3 の body
label when_2
  set_reg_a 33
  jump end_case

label end_case

はい、では入力となる vgtコードを用意して (上のコードを main 関数の中に入れただけ)、

// 21_case.vgt.json

["stmts"
, ["func", "main"
  , []
  , [

      ["case"

      , [ ["eq", 44, 45]    // 条件1
        , ["set_reg_a", 11] // 条件1 が真だった場合に実行
        ]

      , [ ["eq", 55, 56]    // 条件2
        , ["set_reg_a", 22] // 条件2 が真だった場合に実行
        ]

        // else節に相当
      , [ ["eq", 0, 0]
        , ["set_reg_a", 33]
        ]

      ]

    ]
  ]
]

コード生成処理を修正していきましょう。

まずは codegen_case() を追加。 ちょっと分かりにくいかもしれませんが、 それぞれの条件が真だった場合の処理をアセンブリコードの形で then_bodies という配列に溜めておいて、 label end_case の前でまとめて出力するようにしてみました。

# ※ codegen_func_def() の前に追加

def codegen_case(when_blocks)
  alines = []

  when_idx = -1
  then_bodies = []

  when_blocks.each do |when_block|
    when_idx += 1
    cond, *rest = when_block
    cond_head, *cond_rest = cond
    alines << "  # 条件 #{when_idx}: #{cond.inspect}"

    case cond_head
    when "eq"
      alines << "  set_reg_a #{cond_rest[0]}"
      alines << "  set_reg_b #{cond_rest[1]}"
      alines << "  compare"
      alines << "  jump_eq when_#{when_idx}"

      then_alines = ["label when_#{when_idx}"]
      rest.each {|stmt|
        then_alines << "  " + stmt.join(" ")
      }
      then_alines << "  jump end_case"
      then_bodies << then_alines
    else
      raise not_yet_impl("cond_head", cond_head)
    end
  end

  # すべての条件が偽だった場合
  alines << "  jump end_case"

  then_bodies.each {|then_alines|
    alines += then_alines
  }

  alines << "label end_case"

  alines
end

codegen_func_def() から呼び出すようにします。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -108,6 +108,8 @@ def codegen_func_def(rest)
     when "return"
       val = stmt_rest[0]
       alines << "  set_reg_a #{val}"
+    when "case"
+      alines += codegen_case(stmt_rest)
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end

Vm.num_args_for()jump_eq, jump, compare がなくてエラーになったので追加。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -255,9 +255,9 @@ class Vm
     case operator
     when "cp"
       2
-    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp"
+    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp", "jump_eq", "jump"
       1
-    when "ret", "exit", "add_ab"
+    when "ret", "exit", "add_ab", "compare"
       0
     else
       raise "Invalid operator (#{operator})"

vgtコードを書き換えて、 条件1が真になる場合、条件2が真になる場合、 すべての条件が偽になる場合、などの動作を確認して、 よしよし大丈夫だな……とやっていたのですが、 実はまだダメなんですよ。

よしできたぞーどんどん次に進むぞ―といってこのまま進んでしまうと、 忘れた頃に

_人人人人人人人人人人_
> 複数のcase文 <
 ̄YYYYYYYYY

が登場し、謎の挙動にびっくりしてハマることになります (なりました)。

どういうことかというと、case 文が複数存在した場合でも 使っているラベルが when_0 とか end_case とかなので、 ある case 文から別の case 文のラベルにジャンプしてしまうわけですね。

たとえばこんなコードを動かしてみると、

// 21_cases.vgt.json

["stmts"
, ["func", "main"
  , []
  , [

      ["case"
      , [ ["eq", 11, 12]
        , ["set_reg_a", 22]
        ]
      , [ ["eq", 0, 0]
        , ["set_reg_a", 33]  // ここは実行されない
        ]
      ]

    , ["case"
      , [ ["eq", 44, 45]
        , ["set_reg_a", 55]
        ]
      , [ ["eq", 0, 0]
        , ["set_reg_a", 66]
        ]
      ]

    ]
  ]
]

1つ目の case 文の else から 2つ目の case 文の else 節にジャンプしてしまって、 その後すぐ終了してしまいます。

これはダメですね。対策しないと。


それでどうしたかというと、グローバルなインデックスを安直に用意して、 ラベル名に含めるようにしました。

$label_id を直に使わずに label_id にコピーして使っているのは 入れ子の case文を見越しているため。

あと、 $label_id は case 文以外の while 文などでも共通で使う想定です。 while文の中に case文、みたいなのも登場してくるでしょう。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -6,8 +6,12 @@ require 'json'
 
 require './common'
 
+$label_id = 0
+
 def codegen_case(when_blocks)
   alines = []
+  $label_id += 1
+  label_id = $label_id
 
   when_idx = -1
   then_bodies = []
@@ -16,20 +20,20 @@ def codegen_case(when_blocks)
     when_idx += 1
     cond, *rest = when_block
     cond_head, *cond_rest = cond
-    alines << "  # 条件 #{when_idx}: #{cond.inspect}"
+    alines << "  # 条件 #{label_id}_#{when_idx}: #{cond.inspect}"
 
     case cond_head
     when "eq"
       alines << "  set_reg_a #{cond_rest[0]}"
       alines << "  set_reg_b #{cond_rest[1]}"
       alines << "  compare"
-      alines << "  jump_eq when_#{when_idx}"
+      alines << "  jump_eq when_#{label_id}_#{when_idx}"
 
-      then_alines = ["label when_#{when_idx}"]
+      then_alines = ["label when_#{label_id}_#{when_idx}"]
       rest.each {|stmt|
         then_alines << "  " + stmt.join(" ")
       }
-      then_alines << "  jump end_case"
+      then_alines << "  jump end_case_#{label_id}"
       then_bodies << then_alines
     else
       rasie not_yet_impl("cond_head", cond_head)
@@ -37,13 +41,13 @@ def codegen_case(when_blocks)
   end
 
   # すべての条件が偽だった場合
-  alines << "  jump end_case"
+  alines << "  jump end_case_#{label_id}"
 
   then_bodies.each {|then_alines|
     alines += then_alines
   }
 
-  alines << "label end_case"
+  alines << "label end_case_#{label_id}"
 
   alines
 end

OKです! 次に行きましょう!!