細かすぎて見つからなかったmrubyの非互換

Posted by Yutaka HARA on December 06, 2016 · 3 mins read

こんにちは。yhara@NaCl松江本社です。本記事はmruby Advent Calendar 2016の6日目のエントリです。

mrubyとは

mrubyは組み込み用途のために作られたRuby処理系です。言語仕様としてはCRubyとほぼ同じですが、BignumやRegexpがないなど一部に制約があります。mrubyにはmrbgemsというRubyGemsに似た仕組みがあるため、足らない機能はmrbgemsによって補うことができます。

用途としてはハードウェアへの組み込みの他、nginxのようなソフトウェアに組み込んだり、あるいはmrubyプログラムを単一の実行のバイナリにすることができるという特徴からMItamaeのようにサーバ管理の文脈で名前を聞くことも増えています。

CRubyとの互換性

mrubyのコードベースはCRubyとは独立しているため、CRubyにはないバグが存在していることもあります。例えばCRubyのtest/basictestの中にも、mrubyでは動かないものがありました。以下はそれらの、細かすぎて今まで発見されていなかった非互換をまとめたものです。特に実生活に役立ったりはしないと思いますが、「うわー細かい…」と思いながら見てもらえればと思います。

#3235 クラス変数の探索順序が違う

CRubyではモジュールをincludeすることによってクラス変数(以下では@@rule)を上書きできるのですが、これが動いていませんでした。

module Olympians
  @@rule = "Zeus"
end

class Titans
  @@rule = "Cronus"
  include Olympians
  def ruler
    @@rule
  end
end

atlas = Titans.new
p atlas.ruler

#3236, #3239 多重代入の右辺に空配列があると壊れた値が入る

以下を実行するとa, bはnilに、cは空の配列になるはずですが、謎の値(以前に実行したコードの値)が入るというバグがありました。

p [1, 2, 3]  # Some random code

a, b, *c = []
p [a, b, c]   #=> [[1, 2, 3], [[1, 2, 3]], []] が表示される

多重代入はコーナーケースの数が多く、またbreakやreturn等でも多重代入が発生するので、basictestでは入念にテストされていることが分かります。

#3272 ハッシュに直接defaultメソッドを定義した場合

RubyではHash.newにブロックを渡すことで値が存在しなかった場合の挙動をカスタマイズできますが、basictestを読んでいると、ハッシュに特異メソッドとして直接defaultメソッドを定義することでも同様のことができることが分かりました。

h = {}
def h.default(k); self[k] = 0; end
h[:foo] += 1
p h[:foo] # should be 1

CRubyではハッシュへのアクセス時に当該キーが無かった場合、ハッシュに対しdefaultメソッドを呼び出すのでこのようなことができるのですね。mrubyではこれが動かなかったためチケットにしました。

#3273 ヒアドキュメントのあとにセミコロンを付けるとパースエラー

これは非常に微妙な話で、今回報告した中ではこのチケットだけまだ修正されていません。以下のようにヒアドキュメントの終端の指定のあとにセミコロンを付けたとき、CRubyではパースが通る(セミコロンは単に無視される)のですが、mrubyだとパースエラーになります。

a = <<EOD;
hello
EOD

一応メモとして起票してありますが、正直なところこれで困る人もいないと思うので、放置あるいはwontfixでも良い気がします。

#3274 Array#[]=に自分自身を渡した場合

以下のように配列の一部を自分自身で上書きしようとしたときに結果が誤っていました。

x = [1, 2, 3]
x[1, 0] = x; p x
# 正:[1, 1, 2, 3, 2, 3]
# 誤:[1, 1, 1, 1, 2, 3]

ary[i, n] = other というのはaryのi番目からn個の範囲をotherで置き換えるという意味です。例えば以下ではaryの中の0が続いた部分を[9, 9]で置換しています。

ary = [1, 1, 0, 0, 0, 1, 1]
ary[2, 3] = [9, 9]

p ary # [1, 1, 9, 9, 1, 1]

#3302もArray#concatに関する同様の話です。

#3275 forの中でredoした場合の挙動が違う

forの中でredoを呼んだときの挙動が微妙に違っていて、以下のプログラムが無限ループになっていました。

sum = 0
for i in 1..10
  sum += i
  i -= 1
  if i > 0
    redo
  end
end
p sum

eachではこのバグは再現せず、forだけにあるバグだったようです。

まとめ

本稿ではmrubyに存在したCRubyとの微妙な非互換(バグを含む)を紹介しました。細かいものばかりですが、Arrayや多重代入のあたりはひょっとしたら踏む人がいたかも知れないので、事前に見つけられて良かったなと思います。

Ruby処理系のテストとしてはThe Ruby Spec Suite (RubySpecの後継)というのがあり、mrubyでこれを通そうという試みもあるようです。あとCRubyにはbasictestの他にbootstraptestというのがあって、これもMRubyで動かしてみるといろいろ発見があるのではないかなと思うので、誰かチャレンジしてみませんか?