doctree icon indicating copy to clipboard operation
doctree copied to clipboard

両端が数値リテラルの範囲式が評価ごとに違うオブジェクトを返すことがある

Open scivola opened this issue 5 years ago • 5 comments

範囲オブジェクト には,

範囲式はその両端が数値リテラルであれば、何度評価されても同じオブジェク トを返します。

とあります。

実験した範囲では,両端が Integer だと確かにそうでしたが,Float や Rational ではそうなりませんでした。

3.times.map{ 1.0..2.0 }.map(&:object_id).uniq
# => [65553920, 65553900, 65553840]

3.times.map{ 2r..3r }.map(&:object_id).uniq
# => [69043780, 69043760, 69043740]

scivola avatar Nov 20 '18 03:11 scivola

当該の記述は間違いと考えてよさそうなので,

範囲式はその両端が数値リテラルであれば、何度評価されても同じオブジェクトを返します。そうでなければ評価されるたびに新しい範囲オブジェクトを生成します。

の二文は単純削除でいいでしょうか?

scivola avatar Feb 08 '20 12:02 scivola

両端が整数リテラルであれば何度評価されても同じオブジェクトが返るので、「数値リテラル」を「整数リテラル」に置き換えても良いのではないでしょうか。

ちなみに、Bignumであっても同じオブジェクトが返るのかが気になって調べましたが2.4からは同じオブジェクトが返るようです。

$ docker run -it --rm -v /tmp/test.rb:$(pwd)/test.rb rubylang/all-ruby env ALL_RUBY_SINCE=ruby-1.8.0  ./all-ruby -e 'def x() 1..20000000000000000000000000000000000000000000000000000000000000000000000000; end; p x.object_id == x.object_id'    
ruby-1.8.0            false
...
ruby-2.4.0-preview2   false
ruby-2.4.0-preview3   true
...
ruby-2.7.0            true

2.3のドキュメントはすでに生成していないと思うので、「Fixnumでは〜」のような説明は書かなくても良さそうです。

また、begin-less / end-less rangeの場合は同じオブジェクトは返らないようになっています。

$ docker run -it --rm -v /tmp/test.rb:$(pwd)/test.rb rubylang/all-ruby env ALL_RUBY_SINCE=ruby-2.6  ./all-ruby -e 'def x() 1..; end; p x.object_id == x.object_id'
ruby-2.6.0          false
...
ruby-2.7.0          false
$ docker run -it --rm -v /tmp/test.rb:$(pwd)/test.rb rubylang/all-ruby env ALL_RUBY_SINCE=ruby-2.7  ./all-ruby -e 'def x() ..1; end; p x.object_id == x.object_id'
ruby-2.7.0 false

これについては書かなくても良いかなと思います。

pocke avatar Feb 08 '20 13:02 pocke

では第一文の「数値リテラル」を「整数リテラル」(もしくは Integer リテラル)に変え,第二文は削除,としましょうか? 第二文を残すなら,同じオブジェクトを返すのが「両端が Integer」のケース以外に無いということを確認する必要があると思います。

scivola avatar Feb 09 '20 02:02 scivola

第二文を残すなら,同じオブジェクトを返すのが「両端が Integer」のケース以外に無いということを確認する必要があると思います。

確認したところ、両端がIntegerのケース以外は最適化されないようでした。

https://github.com/ruby/ruby/blob/c47cd4be28840159251b4c66df71e10e979316a0/compile.c#L8311-L8332

case NODE_DOT2:
case NODE_DOT3:{
  int excl = type == NODE_DOT3;
  VALUE flag = INT2FIX(excl);
  const NODE *b = node->nd_beg;
  const NODE *e = node->nd_end;
  if (number_literal_p(b) && number_literal_p(e)) {
      if (!popped) {
          VALUE val = rb_range_new(b->nd_lit, e->nd_lit, excl);
          ADD_INSN1(ret, line, putobject, val);
          RB_OBJ_WRITTEN(iseq, Qundef, val);
      }
  }
  else {
      CHECK(COMPILE_(ret, "min", b, popped));
      CHECK(COMPILE_(ret, "max", e, popped));
      if (!popped) {
          ADD_INSN1(ret, line, newrange, flag);
      }
  }
  break;
}

なお、number_literal_pは実際にはIntegerかどうかを見ているようです。 またpoppedは値が評価されない場合の最適化なので、今回の件には関係なさそうですね。 ref: https://github.com/ruby/ruby/commit/451e0a6ee1362b4cfc504087f0d3232bbaeb76ca

pocke avatar Feb 09 '20 07:02 pocke

提示していただいた C のコードが読めないのですが,結論としては「数値リテラル」を「整数リテラル」に変えるのみ(第二文は削除しない),でいいでしょうか?

若干気になるのが,これを仕様としてリファレンスに書いちゃっていいのかどうか,です。今たまたまそういう仕様だけど,将来変わったりしないのか,とか。 将来変わったらリリースノートに載るはず(?)だから気にしなくていいのかな・・・?

scivola avatar Feb 09 '20 10:02 scivola