トップ «前の日記(2014-06-09) 最新 次の日記(2014-06-17)» 編集

Route 477



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で書かれてそうだな。

https://github.com/rubinius/rubinius/blob/d17c1294e09ab9c70495a1278450c09e38ed01c6/kernel/common/symbol.rb#L108-L120

おお、ほぼ上と同じコードになってる。 コメントで、instance_evalのためにselfに名前を付けてると書いてある。たしかにそうか。

報告

ということで、https://github.com/opal/opal/pull/551 に報告しておいた。

Opalのテストをどこに追加すれば良いかわからなかったんだけど、 spec/filters/ 以下に、「このRubySpecはまだ通らないからスキップする」という一覧があって、通るようになったやつをそこから削除すれば良かったようだ。