2014-06-16
■ [ruby][opal] Opalのバグを調査した
QiitaにOpalのTipsをいろいろ投稿したりした。 http://qiita.com/tags/opal OpalはRubyistのためのaltjsで、Rubyっぽいコードを書くとJavaScriptに変換される。今後重要になるRuby処理系だと思う。
ところでOpalにはまだいろいろバグがあるので、そのへんに注意しつつコードを書く必要がある。
今日はmap(&:foo)
がうまく動かないので、原因を調べてみた。
コード例
[[1,2],[3,4]].map(&:last)
#=> CRubyだと[2, 4]を返す
#=> OpalだとNoMethodError: undefined method `last' for 1
デモはこちら。
調査1: to_proc
&:foo
は:foo.to_proc
の省略記法なので、Symbol#to_procの実装を調べてみる必要がありそう。
と思ったがopal/corelib/symbol.rbがないなぁ…ああそうか、OpalにはSymbolがなくて、:foo
は"foo"
と同じなんだった。
opal/corelib/string.rbを見ると、to_procは以下のような定義になっている。
def to_proc
proc do |recv, *args|
recv.send(self, *args)
end
end
つまり[[1,2],[3,4]].map(&:last)
は、
[[1,2],[3,4]].map{|recv, *args|
recv.send("last", *args)
}
というコードと等価なわけだ。
ところがこれは正しくない。ここではrecvに[1,2]
が入ることを期待しているわけだけど、CRubyでeach{|a, *b|
を試すと、以下のように分割されてしまう(Opalでも同様)。
irb(main):004:0> [[1,2],[3,4]].each{|a, *b|
irb(main):005:1* p a: a, b: b
irb(main):006:1> }
{:a=>1, :b=>[2]}
{:a=>3, :b=>[4]}
調査2: CRubyでは?
to_procはCRubyではどのように実装されているのだろうか。githubのruby/rubyをcloneしたやつが手元にあるので、to_procの定義を探してみる。string.cにあるようだ。 その上のsym_callというのがイテレータらしい。
static VALUE
sym_call(VALUE args, VALUE sym, int argc, VALUE *argv, VALUE passed_proc)
{
VALUE obj;
if (argc < 1) {
rb_raise(rb_eArgError, "no receiver given");
}
obj = argv[0];
return rb_funcall_with_block(obj, (ID)sym, argc - 1, argv + 1, passed_proc);
}
移植
とりあえずpassed_procを無視すると、Opalへの移植はこうなるかな。
def to_proc
proc do |*args|
if args.length < 1
raise ArgumentError, "no receiver given"
end
obj = args.shift
obj.send(self, *args)
end
end
うん、動いてそう。
passed_procは、block.callにブロックを渡した場合に使われるんだろうな。こんな感じでいいのかな。
def to_proc
proc do |*args, &block|
if args.length < 1
raise ArgumentError, "no receiver given"
end
obj = args.shift
obj.send(self, *args, &block)
end
end
実際の例はこう。そういえばどっかのバージョン(Ruby 1.9か2.0か)から、ブロックがブロックを取れるようになったんだっけ。
foo = proc{|a, &b|
b.call(a)
}
foo.call(1){|x| puts "I got #{x}" }
調査3: Rubinius
そういえばRubiniusのto_procはRubyで書かれてそうだな。
おお、ほぼ上と同じコードになってる。 コメントで、instance_evalのためにselfに名前を付けてると書いてある。たしかにそうか。
報告
ということで、https://github.com/opal/opal/pull/551 に報告しておいた。
Opalのテストをどこに追加すれば良いかわからなかったんだけど、 spec/filters/ 以下に、「このRubySpecはまだ通らないからスキップする」という一覧があって、通るようになったやつをそこから削除すれば良かったようだ。