LoginSignup
1
0

More than 5 years have passed since last update.

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

Posted at

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

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

if

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

これをRubyに移植する。

if 条件
  処理1
else
  処理2
end

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

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

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

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

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

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

block

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

(block
  処理1
  br 0
  処理2
)
while true
  処理1
  break
  処理2

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

loop

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

(loop
  処理1
  br 0
  処理2
)
while true
  処理1
  next
  処理2

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

多段

block→if

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

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

  break # 必ず抜ける
end

block→block

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

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

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

        break # 必ず抜ける
      end
    end

    break # 必ず抜ける
  end
end

loop→block

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

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

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の扱いを変えなければいけないのは面倒すぎる!

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

# 第二引数は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

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

;; 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)
  )
)

↑これが、↓こうなる。

;; 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の違いをまったく意識しなくても良くなった。

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

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0