2021/12/09 今回の気になった bugs.ruby のチケット

今週は import_methods に C拡張で定義されたモジュールを渡せないバグ報告などがありました。

[Bug #18396] An unexpected "hash value omission" syntax error when without parentheses call expr follows

  • Ruby 3.1 では Hash の省略記法が新しく入ったが次のようなコードの時に『次の行の式』が値になってしまうというバグ報告
# error: syntax error, unexpected local variable or method, expecting `do' or '{' or '('
foo key:
foo arg
  • もしくは以下のような感じ
def hoge(**kwd)
  pp kwd
end

key = 42

# 意図としては hoge(key: key) となってほしい
# しかし実際には hoge(key: 1 + 2) と解釈される
hoge key:
1 + 2
# => {:key=>3}
  • Ruby 3.0 でも以下のように動作するのでこれは仕様とのこと
foo key:
bar
  • 今回の場合は () を記述する事で回避する事ができる
def hoge(**kwd)
  pp kwd
end

key = 42

hoge(key:)  # => {:key=>42}
1 + 2

[Feature #18395] Introduce Array#subtract! for performance

  • レシーバから特定の要素を取り除く Array#subtract! を追加する提案
ary = [0, 1, 1, 2, 1, 1, 3, 1, 1]
ary.subtract!([1]) #=> [0, 2, 3]
ary                #=> [0, 2, 3]

ary = [0, 1, 2, 3]
ary.subtract!([3, 0], [1, 3]) #=> [2]
ary                           #=> [2]
  • これは Array#- と似たようなメソッドになる
ary = [0, 1, 1, 2, 1, 1, 3, 1, 1]
ary - [1] #=> [0, 2, 3]

# これが ary.subtract! と同じ挙動になる
ary -= [1] #=> [0, 2, 3]
ary        #=> [0, 2, 3]
  • ary -= othetr を高速化するためのメソッドとして ary.subtract!(other) を追加したらしい
    • #subtract! の方が 1.22倍早いらしい
$ cat test.rb
require 'benchmark'

b_array = [2, 2, 2, 2, 2, 5, 5, 5, 5, 5, 99]

n = 5_000_000
Benchmark.bm do |x|
  x.report("Array#subtract!") { n.times do; a = [1, 2, 3]; a.subtract!(b_array); end }
  x.report("Array#-=   ") { n.times do; a = [1, 2, 3]; a -= b_array; end }
end
$ make runruby
generating vm_call_iseq_optimized.inc
vm_call_iseq_optimized.inc unchanged
RUBY_ON_BUG='gdb -x ./.gdbinit -p' ./miniruby -I./lib -I. -I.ext/common  ./tool/runruby.rb --extout=.ext  -- --disable-gems  ./test.rb
       user     system      total        real
able-gems  ./test.rb
       user     system      total        real
Array#subtract!  1.168282   0.002174   1.170456 (  1.172227)
Array#-=     1.428345   0.004802   1.433147 (  1.439785)
$ cat test.rb
require 'benchmark'

b_array = [2, 2, 2, 2, 2, 5, 5, 5, 5, 5, 99]

n = 5_000_000
Benchmark.bm do |x|
  x.report("Array#concat") { n.times do; a = [1, 2, 3]; a.concat(b_array); end }
  x.report("Array#+=    ") { n.times do; a = [1, 2, 3]; a += b_array  ; end }
end
$ make runruby
compiling version.c
generating vm_call_iseq_optimized.inc
vm_call_iseq_optimized.inc unchanged
linking miniruby
/bin/sh ./tool/ifchange "--timestamp=.rbconfig.time" rbconfig.rb rbconfig.tmp
rbconfig.rb unchanged
creating verconf.h
verconf.h updated
compiling loadpath.c
builtin_binary.inc updated
compiling builtin.c
linking static-library libruby.3.1-static.a
linking ruby
RUBY_ON_BUG='gdb -x ./.gdbinit -p' ./miniruby -I./lib -I. -I.ext/common  ./tool/runruby.rb --extout=.ext  -- --disable-gems  ./test.rb
       user     system      total        real
Array#concat  0.785914   0.004957   0.790871 (  0.794673)
Array#+=      0.752269   0.007611   0.759880 (  0.767160)
  • 『このメソッドを使うと高速化する』っていうのを意識して書きたくないので -=演算子自体が高速化しないですかねー

[PR debug #408] The console prevents Zeitwerk from autoloading certain constants

  • debug.gem のコンソールで Zeitwerk の autoload が動作しないというバグ報告
  • これは TracePoint がネストして呼び出されるような形になっているのが原因ぽい
  • これに対応する手段として『 TracePoint をネストしても呼び出せるようにする』方向で議論が進んでるっぽい?

[Feature #15912] Allow some reentrancy during TracePoint events

  • 上で書いた TracePoint のネストを許容するチケット
  • 元々は byebug x Zeitwerk での問題が起因だったが debug.gem でも同様の問題が発生しているのでこちらで議論されている
  • Ruby 3.1 までに結論が出てくれると嬉しいですが時期的にどうだろうなあ…

[Bug #18385] Refinement#import_methods(Enumerable) doesn't work

  • 次のように String クラスに対して import_methods Enumerable するとエラーになるというバグ報告
module M
  refine String do
    # error: `import_methods': Can't import method: Enumerable#drop (ArgumentError)
    import_methods Enumerable
  end
end
  • これは import_methods が C拡張で定義されたモジュールに対応していない事が原因らしい
  • このチケットではこのバグを明確にするためにエラーメッセージを修正して対応している
module M
  refine String do
    # error `import_methods': Can't import method which is not defined with Ruby code: Enumerable#drop (ArgumentError)
    import_methods Enumerable
  end
end
  • 根本的な対応はどうするんですかね、これ

[Feature #18367] Stop the interpreter from escaping error messages

  • エラーメッセージをエスケープしないようにする提案
class MyError < StandardError
  def message
    "foo\\bar"
  end
end

raise MyError
#=> current:  test.rb:7: in `<main>': foo\\bar (MyError)
#=> excepted: test.rb:7: in `<main>': foo\bar (MyError)
  • これによってエスケープシーケンスもエスケープされるのでエラーメッセージをハイライトする事ができない
# エラーメッセージを赤色で表示したいができない
$ ruby -e 'raise "\e[31mRed\x1b[0m error"'
-e:1:in `<main>': \e[31mRed\x1b[0m error (RuntimeError)
  • 他には "\\" に対するエラーメッセージが紛らわしかったり error_highlight の位置もずれてしまう問題があるみたい
$ ruby -e '"\\".no_method'
-e:1:in `<main>': undefined method `no_method' for "\\\\":String (NoMethodError)

"\\\\".no_method
    ^^^^^^^^^^

[Feature #18279] ENV.merge! support multiple arguments as Hash.merge!

  • Hash#merge! と同様に ENV.merge! に複数の Hash を渡せるようにするチケット
  • 以下のようなケースで使いたいらしい
require 'yaml'
env_files = ['config.yml', 'config.local']
envs = env_files.filter_map {|file| YAML.load_file(file)['env'] if File.file?(file) }
ENV.merge!(*envs) # Raise wrong number of arguments (given 2, expected 1)

# 現状はこういうコードを書いているらしい
# ENV.merge!({}.merge!(*envs))
  • Hash でできることは ENV でもしたいと思うのでこの対応はよさそうですね

[Misc #18354] Lazily create singletons on instance_{exec,eval}

  • instance_{exec, eval} を呼び出した時に特異クラスが生成され、ブロック内でメソッドを定義するとその特異クラスにメソッドが追加される
obj = Object.new
obj.instance_eval {
  # このメソッドは obj の特異クラスに定義される
  def hoge
    "hoge"
  end
}
p obj.hoge
# => "hoge"
  • このチケットはメソッド定義に必要な場合を除いて特異クラスの生成を遅延させる内容になっている
  • 遅延することによって Railsベンチマークが 1.09倍早くなり、YJIT だと 1.16倍早くなったらしい
  • また今回の対応によって TrueCLass/FalseClass/NilClass が他のクラスと異なる挙動になっていたがこれも同じ挙動になるようになる
String::Foo = "foo"
p "".instance_eval("Foo")
# => "foo"

Integer::Foo = "foo"
p 123.instance_eval("Foo")
# => "foo"

TrueClass::Foo = "foo"
p true.instance_eval("Foo")
# Ruby 3.0 => NameError: uninitialized constant Foo
# Ruby 3.1 => "foo"