WebAssemblyのifとblockとloopをRubyで表す

WebAssemblyをRubyにコンパイルするツールを作っている。

https://github.com/edvakf/wagyu

今日はWebAssemblyのif,block,loopとbr命令について考える。

## if

```lisp
(if 条件
  (then
    処理1
  )
  (else
    処理2
  )
)
```

これをRubyに移植する。

```ruby
if 条件
  処理1
else
  処理2
end
```

と簡単にはいかない。ifの中でbr命令が出てくると、if節の最後までジャンプすることになる。

```lisp
(if 条件
  (then
    br 0
    処理1
  )
  (else
    処理2
  )
)
```

この場合はthenに入ったら処理1は実行されずにif節を抜ける。

この挙動はRubyにそのまま移植することはできない。こういうときのお決まりの方法はこれ。

```ruby
catch(:foo) do
  if 条件
    throw :foo
    処理1
  else
    処理2
  end
end
```

次にblockとloopを考える。Rubyだとどちらもwhileで書ける。

## block

block節もその中にbrがあるとループの最後にジャンプする。↓この例だと処理1は実行されるが処理2は実行されない。

```lisp
(block
  処理1
  br 0
  処理2
)
```

```ruby
while true
  処理1
  break
  処理2

  break # brを呼ばなかったとしてもここで必ず抜ける
end
```

## loop

逆にloopはその中でbrがあると、節の頭にジャンプする。↓この例だと処理1が無限に実行され続ける。

```lisp
(loop
  処理1
  br 0
  処理2
)
```

```ruby
while true
  処理1
  next
  処理2

  break # brを呼ばなかったらここで必ず抜ける
end
```

# 多段

## block→if

```lisp
(block
  (if 条件
    (then
      br 1 ;; 0だとif節から抜けるが、1だとblock節から抜ける
    )
    (else
    )
  )
)
```

```ruby
while true

  catch(:foo) do
    if 条件
      break # 0だとthrowだが、1だとbreakになる
    else
    end
  end

  break # 必ず抜ける
end
```

## block→block

```lisp
(block
  (block
    br 1 ;; 外側のblockを抜ける
  )
)
```

Rubyではラベルつきbreakが無いので、blockでもcatchを使ったほうがいい、ということになる。

```ruby
catch(:foo) do
  while true
    catch(:bar) do
      while true
        throw :foo # 0だとbarだが、1だとfooになる

        break # 必ず抜ける
      end
    end

    break # 必ず抜ける
  end
end
```

## loop→block

```lisp
(loop
  (block
    br 1 ;; 外側のloopを繰り返す
  )
)
```

これだとcatchでもダメで、nextするための変数を導入するなどしなければならない。

```ruby
while true
  bar = catch(:bar) do
    while true
      throw :bar, :again # throwの第二引数はcatchの返り値になる

      break # 必ず抜ける
    end
  end
  next if bar == :again

  break # 必ず抜ける
end
```

# めんどくさい

if,block,loopによって、ネストの仕方によってbrの扱いを変えなければいけないのは面倒すぎる！

と思って考えた結果、メソッドに切り出すことにした。

https://github.com/edvakf/wagyu/blob/7ccfd0e4b1b60d6820d164aaf39485f0f14b2a31/lib/wagyu/wasm/module.rb#L12-L32

```ruby
# 第二引数はprocで第三引数はblock
def _if(condition, then_proc, &else_block)
  if condition
    then_proc.call
  elsif else_block
    yield else_block
  else
    -1
  end
end

# 自分を指しているbrのみnextにする
def _loop(&block)
  while true
    depth = yield block
    next if depth == 0
    return depth
  end
end

# 実行するだけ
def _block(&block)
  yield block
end
```

これだと、何が良いかというと、

```lisp
;; 1からnumまで足した結果を返す
(module
  (func (export "sum") (param $num i32) (result i32)
    (local $sum i32) ;; int sum
    (local $i i32) ;; int i

    (set_local $sum (i32.const 0)) ;; sum = 0
    (set_local $i (i32.const 1)) ;; i = 1

    ;; while(true)
    (block $block (loop $loop
      (br_if $block (i32.gt_u (get_local $i) (get_local $num))) ;; if (i >= num) break

      ;; inside loop
      (set_local $sum (i32.add (get_local $sum) (get_local $i))) ;; sum = sum + i

      (set_local $i (i32.add (get_local $i) (i32.const 1))) ;; i = i + 1
      (br $loop) ;; continue
    ))

    (get_local $sum)
  )
)
```

↑これが、↓こうなる。

```ruby
;; 1からnumまで足した結果を返す
  def sum(num)
    sum = 0
    i = 1
    depth = _block{
      depth = _loop{
        next 1 if i > num # brではなくbr_if命令
        sum = sum + i
        i = i + 1
        next 0
        -1
      }
      next depth - 1 if depth > 0
      -1
    }
    sum
  end
```

* ifでもblockでもloopでも必ず `depth = ` で始めれば良い
* brは `next 深さ` に変換する
* 正常に抜けるときは -1 を返す
* どれだけ多段ネストしてもブロックの終わりに `next depth - 1 if depth > 0` をつければよい（トップレベルだけは省略する）

if,block,loopの違いをまったく意識しなくても良くなった。

パフォーマンスは計測してない。