2021/07/08 今週の気になった bugs.ruby のチケット

今週は Ruby 3.1.0 で default gem から bundled gem に移動するチケットなどを載せています。

[Feature #17873] Update of default gems in Ruby 3.1

  • Ruby 3.1 からいくつかの default gem が bundled gem に変更されるチケット
  • 現時点で(あくまでも現時点で)は以下の default gem が bundled gem に変更されるので注意する必要がある
  • また以下の gem は default gem から取り除かれます
    • tracer
    • dbm
    • gdbm
  • default gem と bundled gem の違い
    • default gem
      • Ruby 本体にバンドルされている gem
      • gem 単体で更新可能
      • gem 自体を削除するのは不可能
      • bundler 環境でも使用可能
    • bundled gem
      • Ruby 本体にバンドルされている gem
        • gem 単体で更新可能
        • gem 自体を削除するのは可能
        • bundler 環境では使用不可能
          • 使用する場合は明示的に Gemfilegem "xxx" と書いておく必要がある
    • bundled gem と default gem の違いの具体例 - @znz blog
  • ちなみにこの影響で capypara などが壊れたらしい

[Bug #16243 完了] case/when is slower than if on MRI

  • Ruby 2.6.5 で if 文よりも case when 文の方が遅いというバグ報告
# frozen_string_literal: true
require "benchmark/ips"

def deep_dup_case(obj)
  case obj
  when Integer, Float, TrueClass, FalseClass, NilClass
    obj
  when String
    obj.dup
  when Array
    obj.map { |e| deep_dup_case(e) }
  when Hash
    duped = obj.dup
    duped.each_pair do |key, value|
      duped[key] = deep_dup_case(value)
    end
  else
    obj.dup
  end
end

def deep_dup_if(obj)
  if Integer === obj || Float === obj || TrueClass === obj || FalseClass === obj || NilClass === obj
    obj
  elsif String === obj
    obj.dup
  elsif Array === obj
    obj.map { |e| deep_dup_if(e) }
  elsif Hash === obj
    duped = obj.dup
    duped.each_pair do |key, value|
      duped[key] = deep_dup_if(value)
    end
    duped
  else
    obj.dup
  end
end


obj = { "class" => "FooWorker", "args" => [1, 2, 3, "foobar"], "jid" => "123987123" }

Benchmark.ips do |x|
  x.report("deep_dup_case") do
    deep_dup_case(obj)
  end

  x.report("deep_dup_if") do
    deep_dup_if(obj)
  end

  x.compare!
end
Warming up --------------------------------------
       deep_dup_case    37.767k i/100ms
         deep_dup_if    41.802k i/100ms
Calculating -------------------------------------
       deep_dup_case    408.046k (± 0.9%) i/s -      2.077M in   5.090997s
         deep_dup_if    456.657k (± 0.9%) i/s -      2.299M in   5.035040s

Comparison:
         deep_dup_if:   456657.4 i/s
       deep_dup_case:   408046.1 i/s - 1.12x  slower
  • 最新版では改善されているそうなのでシュッと閉じられている

2021/07/01 今週の気になった bugs.ruby のチケット

今週はエラー箇所をハイライトする error_highlight という gem が本体に追加されました。

[Feature #17930] Add column information into error backtrace

  • エラー箇所をハイライトする機能の提案
  • error_highlight という gem として取り込まれた
  • Ruby 3.1.0 以降では以下のようにエラー箇所が ^ でハイライトされるようになった
$ ruby -e '"1234".times { }'
-e:1:in `<main>': undefined method `times' for "1234":String (NoMethodError)

"1234".times { }
      ^^^^^^
$
  • ただし、エラー行にマルチバイト文字や一部の記号が含まれている場合うまくハイライトされないので注意
$ ruby -e '1.あいうえお {}'
-e:1:in `<main>': undefined method `あいうえお' for 1:Integer (NoMethodError)

1.あいうえお {}
 ^^^^^^
$
$ ruby -e '"\"#{1234}\"".times { }'
-e:1:in `<main>': undefined method `times' for "\\"1234\\"":String (NoMethodError)

"\\"#{1234}\\"".times { }
             ^^^^^^
$
  • つらいので直したいが直すのもつらい…

[Feature #17881] Add a Module#const_added callback

  • 背景としては zeitwerk でオートロードを実装する際に TracePoint を使っているのだが TracePoint はデバッグ用の API なので本番で使われるのはつらい
  • 他にも beybug などと競合する
  • これを解消する手段として Module#const_added を追加しようという提案
    • method_added みたいに未定義の定数が追加された時にコールバックするメソッド
  • 最適化や MJIT 側の対応について議論などしていたが1ヶ月ぐらい前で止まっちゃってるので特に進展はなさそう…

2021/06/24 今週の気になった bugs.ruby のチケット

今週はエラー箇所をマークする gem の PR などがありました。

[Bug #14817] TracePoint#parameters for bmethod's return event should return the same value as its Method#parameters

  • TracePoint の :return イベント時に TracePoint#parameters で正しく値が取得できないバグ報告
define_method(:bm) {|a|}

p method_parameters: method(:bm).parameters
# => {:method_parameters=>[[:req, :a]]}

trace = TracePoint.new(:call, :return){|tp|
  mid = tp.method_id
  if mid == :bm
    p mid: mid, event: tp.event, tp_parameters: tp.parameters
  end
}
trace.enable{
  bm(0)
}

# :call 時は parameters が取得できているが
# :return 時は parameters が取得できてない
# output:
# {:mid=>:bm, :event=>:call, :tp_parameters=>[[:req, :a]]}
# {:mid=>:bm, :event=>:return, :tp_parameters=>[]}
define_method(:bm) {|a|}

trace = TracePoint.new(:call, :return){|tp|
  p [tp.event, tp.lineno] if tp.method_id == :bm
}
trace.enable{
  bm(0)
}
# output:
# [:call, 1]
# [:return, 7] #=> [:return, 1] になるべき?

[Bug #14391] Integer#digitsが遅い

  • Integer#digits が遅いというバグ報告
  • Integer#to_s と比較してもかなり遅いらしい
(9999**9999).to_s.chars.map(&:to_i).reverse # 0.030225秒
(9999**9999).digits # 1.187126秒 (40倍)

(99999**99999).to_s.chars.map(&:to_i).reverse # 1.888218秒
(99999**99999).digits # 195.594539秒 (100倍)
pp 16.digits    # => [6, 1]
pp 1234.digits  # => [4, 3, 2, 1]

[PR 4414] [WIP] add error_squiggle gem

  • エラー箇所をマークする gem が開発されてるよ、っていう話
$ ./local/bin/ruby -e '1.time {}'
-e:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError)

1.time {}
 ^^^^^
Did you mean?  times
$ ./ruby -e 'ああああ.time {}'
-e:1:in `<main>': undefined local variable or method `ああああ' for main:Object (NameError)

ああああ.time {}
^^^^^^^^^^^^
$ ./ruby -e '1.あああ {}'
-e:1:in `<main>': undefined method `あああ' for 1:Integer (NoMethodError)

1.あああ {}
 ^^^^
  • 原因自体はわかっているんだけどマルチバイト文字で表示幅を計算する方法が現状 reline の内部 API にしか存在していないのでつらい

[Feature #18004] Add Async to the stdlib

require 'async'

def sleepy(duration = 3)
  Async do |task|
    task.sleep duration
    puts "I'm done sleeping, time for action!"
  end
end

# 同期処理
sleepy

# 非同期処理
Async do
  # 同時に sleep が実行される
  sleepy
  sleepy
end

2021/06/17 今週の気になった bugs.ruby のチケット

今週は ENV に対して変更が入りました。

[Bug #17098] Float#negative? reports negative zero as not negative

  • -0.0Float#negative?false を返すのは期待しているのか?というバグ報告
    • IEEE 754 の規格?でそう定まってるらしい?
neg_zero = -0.0
pp neg_zero.negative?
# => false
pp neg_zero < 0
# => false
  • チケットを立てた人が期待する動作としてはこう
-0.0.negative?
# => true

0.0.positive?
# => false
  • その他、現状の挙動
pp RUBY_VERSION  # => "3.0.1"


# これは両方共 true
pp 0.0 == 0.0    # => true
pp -0.0 == -0.0  # => true

# equal? の場合
pp 0.0.equal?(0.0)    # => true
pp -0.0.equal?(-0.0)  # => false

# 変数に代入してから比較すると
a = -0.0
pp a.equal?(a)  # => true

puts "---"
# -0.0 か判定する方法
# https://bugs.ruby-lang.org/issues/17098#note-7
def check_negative_zero(f)
  1.0 / f == -Float::INFINITY
end
pp check_negative_zero(-0.0)
# => true
pp check_negative_zero(0.0)
# => false

[Feature #17950] Unable to pattern-match against a String key

  • パターンマッチでキーが String の場合にエラーになるというバグ報告
# Hash のキーに文字列がある場合、マッチする事ができない
case { status: 200, headers: {"content-type" => "application/json"}, body: "bla" }
in { status: , headers: { "content-type" => type }, body: }
end
# syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END
# ...tus: , headers: {"content-type" => type}, body: }
case { status: 200, headers: {"content-type" => "application/json"}, body: "bla" }
in { status: , headers: headers, body: } if type = headers["content-type"]
  pp type
  # => "application/json"
end
  • 早くパターンマッチ使いたい

[Bug #17767] Cloned ENV inconsistently returns ENV or self

  • ENVENV.clone したオブジェクトで挙動に一貫性がないというバグ報告
cloned_env = ENV.clone

p ENV.each_key{}.equal?(ENV) #=> true
p cloned_env.each_key{}.equal?(cloned_env) #=> true

ENV.delete('TEST')

err = ENV.fetch('TEST') rescue $!
p err.receiver.equal?(ENV) #=> true
err = cloned_env.fetch('TEST') rescue $!
p err.receiver.equal?(cloned_env) #=> false

ENV['TEST'] = 'TRUE'
p ENV.select!{ false }.equal?(ENV) #=> true

cloned_env['TEST'] = 'TRUE'
p cloned_env.select!{ false }.equal?(cloned_env) #=> false

[Bug #17951] Collisions in Proc#hash values for blocks defined at the same line

  • Proc#hash の値が同じ値になるケースがあるというバグ報告
require 'set'

def capture(&block)
  block
end

# 同じブロックを大量に生成する
blocks = Array.new(1000) { capture { :foo } }

hashes = blocks.map(&:hash).uniq
ids = blocks.map(&:object_id).uniq
equality = blocks.map { blocks[0].eql?(_1) }.tally
hash = blocks.to_h { [_1, nil] }
set = blocks.to_set

# hash が一意であれば hashes.size == 1000 になるはずだがなっていない
puts(hashes.size)      # => 11
puts(ids.size)         # => 1000
puts(equality.inspect) # => {true=>1, false=>999}
puts(hash.size)        # => 1000
puts(set.size)         # => 1000

[Bug #15993] 'require' doesn't work if there are Cyrillic chars in the path to Ruby dir

D:\users\киї\Ruby\2.6\bin>ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x64-mingw32]

D:\users\киї\Ruby\2.6\bin>ruby -e "require 'logger'"
Traceback (most recent call last):
        1: from <internal:gem_prelude>:2:in `<internal:gem_prelude>'
<internal:gem_prelude>:2:in `require': No such file or directory -- D:/users/РєРёС—/Ruby/2.6/lib/ruby/2.6.0/rubygems.rb (LoadError)
  • チケットでは Ruby 2.6.3 で再現すると書かれている
    • これ自体 2年前のチケット
  • Ruby 2.7 以降では既に直っているので Close されている
D:\Евгений>C:\Ruby26-x64\bin\ruby -I D:\Евгений -e "require 'logger'"
Traceback (most recent call last):
        2: from -e:1:in `<main>'
        1: from C:/Ruby26-x64/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
C:/Ruby26-x64/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require': No such file or directory -- D:/Евгений/logger.rb (LoadError)

D:\Евгений>C:\Ruby27-x64\bin\ruby -I D:\Евгений -e "require 'logger'"

D:\Евгений>C:\Ruby30-x64\bin\ruby -I D:\Евгений -e "require 'logger'"
  • どうやって直ったのかが気になる…

2021/06/10 今週の気になった bugs.ruby のチケット

今週は TypeScript のようなコンストラクタを Ruby に導入したい、というチケットなどがありました。

[Feature #17942] Add a initialize(public @a, private @b) shortcut syntax for defining public/private accessors for instance vars as part of constructor

  • TypeScript だと以下のようにコンストラクタのショートハンドを記述できる
class Foo {
    constructor(public a:number, public b:number, private c:number) {
    }
}
  • 上記は以下と同じ意味
class Foo {
    constructor(a, b, c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }
}
  • これと同じような形で Ruby#initializeインスタンス変数に割り当てるようにする提案
class Thing
  def initialize(public @a, public @b, @c)
  end
end
  • 上記は以下と同じ意味
class Thing
  attr_accessor :a, :b

  def initialize(a, b, c)
    @a = a
    @b = b
    @c = c
  end
end
  • 個人的には強く #initialize に依存してしまっている機能なのでちょっとイマイチ
    • #initialize はただのコールバックメソッドに過ぎない
    • メソッドでしかないので他のメソッドでも使えないと一貫性がない
    • 仮引数はどうやって参照するの?
    • アクセッサまで定義されてるのは意味が強すぎる
  • Ruby ではあとから #initialize も再定義できるのでその場合にどうなるのかが気になる
class Thing
  def initialize(public @a, public @b, @c)
  end
end

class Thing
  # こう再定義したらどうなるの?
  def initialize(@a, public @a)
  end
end
  • どうせやるなら宣言的にやったほうがいい気はする
    • が、これやるなら gem でいいじゃんって思う
class Thing
  initialize(public: [:a, :b], private: :c) do
  end
end
class Thing < Struct.new(:a, :b, :c)
end

[Feature #17938] Keyword alternative for boolean positional arguments

  • bool 値を渡している位置引数をキーワード引数に置き換えようという提案
  • 例えば以下のメソッドは true / false で挙動を制御できるがそれが何を意味しているのかがわからない
object.respond_to?(:symbol, false) # what does `false` mean?
object.methods(true) # what does `true` mean?
  • なので以下のようにキーワード引数にしようという提案
object.respond_to?(:symbol, include_all: false)
object.methods(regular: true)
# or
object.methods(only_public: true)
# or
object.methods(include_all: false)
  • 実装案は以下のように非互換にならないようするイメージ
def respond_to?(symbol, include_all_positional=false, include_all: nil)
  include_all ||= include_all_positional

  # ...
end
obj = Object.new
obj.respond_to?(:symbol, include_all = false)
# or
obj.methods(include_inherited = true)
  • これは Ruby の問題じゃなくてエディタ側でカバーすべきでは?みたいなコメントもある
  • キーワード引数にした場合のキーの名前をどうするのかとパフォーマンスの懸念点が上げられている

[Bug #17925] Pattern matching syntax using semicolon one-line

  • case expression in 42; end とワンラインでパターンマッチを記述した場合にエラーになるのは意図する挙動なのか?というバグチケット
  • case expression when 42; end みたいに when だおt問題ない
  • case expression; in 42; end みたいに ; で区切ると問題ない
  • これは case (expression in 42); end と解釈されてしまっているのが問題らしい

[Bug #17937] Segmentation fault in Enumerator#next on Apple M1, Mac OS Big Sur 11.2.2

  • Ruby 2.7.1 + Apple M1, Mac OS Big Sur 11.2.2 で [1,2,3].to_enum.next を実行すると Segv するというバグ報告
    • どうして…
  • Ruby 2.7.2 以降と 3.0.1 以降では直っている

[Feature #17930] Add column information into error backtrace

def target
  # caller_locations[0] は自身のメソッドの呼び出し元の情報を保持している
  RubyVM::AbstractSyntaxTree.of(caller_locations[0])
end

p target #=> #<RubyVM::AbstractSyntaxTree::Node:VCALL@5:2-5:8>
# ^^^^^^ Line 5, Column 2--8

2021/06/04 今週の気になった bugs.ruby のチケット

今週は backtrace に情報を追加する提案がありました。

[Misc #17932] 90s design (please lets move to 21st century)

  • バグトラッカーを古いデザインから新しいデザインにしようぜ!っていうチケット
  • github へディスカッションを移動しようぜ!みたいな内容
  • この人が言うには bugs.ruby の検索が壊れているらしいが、結局一時的におかしくなっていただけで現状は問題ないっぽい
  • 現状は ruby/b.r-l.o の方で議論するようにコメントされてチケット自体や Reject されている
  • これとは関係ないが bugs.rubyruby/rubygithub と各 bundled gem のリポジトリのどこで議論するのか分からん問題があるので、そのあたりをルール化してほしい気持ちは無きにしもあらず

[Feature #17930] Add column information into error backtrace

  • backtrace にエラー位置情報を追加する提案
  • 具体的には以下のような情報を追加するイメージ
    • Thread::BacktraceLocation#first_lineno
    • Thread::BacktraceLocation#first_column
    • Thread::BacktraceLocation#last_lineno
    • Thread::BacktraceLocation#last_column
  • こういう情報を追加することで以下のように『エラー箇所を明示化できる』ようにできるユースケースが提示されている
$ ruby -r ./sample/no_method_error_ext.rb err1.rb
err1.rb:2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)

data["data"].first["field"]
                  ^^^^^^^^^
  • undefined method `[]' for nil:NilClass (NoMethodError) だけだとどこでエラーになっているのかわからないのでこれは便利そう
    • datanil なのか data["data"].firstnil なのかがわからない

[Bug #17889] Enumerator::Lazy#with_index should return size

  • Enumerator::Lazy#with_index の戻り値に対して size を呼ぶと意図しない値が返ってきたというバグ報告
p Enumerator::Lazy.new([1, 2, 3], 3){|y, v| y << v}.with_index.size
# 期待する値 => 3
# 実際の値   => nil

2021/05/30 今週の気になった bugs.ruby のチケット

今週は prepend した際の定数探索のバグ報告がありました。

[Bug #17887] Missed constant lookup after prepend

  • 次のように prepend したときの定数の探索がおかしいというバグ報告
class A
  FOO = 'a'
end

class B < A
  def foo
    FOO
  end
end

b = B.new

# スーパークラスの FOO を参照するのでこれは正しい
p b.foo
# => "a"

module M
  FOO = 'm'
end
A.prepend M

# A に prepend すると B の継承リストはこうなる
p B.ancestors
# => [B, M, A, Object, Kernel, BasicObject]

# A よりも M が前にあるので M::FOO を参照してほしいが実際には A::FOO を参照している
p b.foo
# 期待する挙動 => "m"
# 実際の挙動   => "a"
  • ちなみに B.include M をすると期待する動作をする
class A
  FOO = 'a'
end

class B < A
  def foo
    FOO
  end
end

b = B.new

module M
  FOO = 'm'
end
B.include M

# A.prepend M と同じ継承リスト
p B.ancestors
# => [B, M, A, Object, Kernel, BasicObject]

# これは期待する挙動になる
p b.foo
# => "m"

[Feature #12913] A way to configure the default maximum width of pp

  • pp で表示幅を指定する時に PP.default_maxwidth みたいなグローバル値で設定できるようにする提案
require "pp"

# 現状幅を指定する場合は第三引数に幅数を渡す必要がある
# NOTE: チケットで提示されていたのは pp だけど現状は動かなくなってるぽい
PP.pp([1, 2, 3], $>, 1)
# => [1,
#     2,
#     3]
  • グローバル値だと意図しない箇所にまで範囲にまで影響を受けてしまうので危険だとコメントされている
  • 現在は PP.ppKernel#ppwidth: キーワード引数を受け取るような形で提案がされている
pp([1, 2, 3], width: 1)
  • ただし、この実装だと現在の挙動と非互換になる
# 現状は Hash として出力される
pp([1, 2, 3], width: 1)
# => [1, 2, 3]
#    {:width=>1}