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

Integer.try_convert の追加や Struct.new.keyword_init? の追加などがありました。

[Feature #10473] Change Date#to_datetime to use local time

require "date"

# ローカルのタイムゾーンを使用しない
p Date.new(2014,1,1).to_datetime.to_time
# => 2014-01-01 00:00:00 +0000

# to_date や to_time はローカルのタイムゾーンを参照している
p Date.new(2014,1,1).to_date.to_time
# => 2014-01-01 00:00:00 +0900
p Date.new(2014,1,1).to_time
# => 2014-01-01 00:00:00 +0900
p Time.parse('2021-07-23')
# => 2021-07-23 00:00:00 +0900

p DateTime.parse('2021-07-23')
# => #<DateTime: 2021-07-23T00:00:00+00:00 ((2459419j,0s,0n),+0s,2299161j)>
  • また、 to_datetime でローカルのタイムゾーンを使用するようにすると次の結果が変わってしまう
DateTime.parse(d.to_s) == d.to_datetime
  • DateTime 自体、互換性のために残されているので非互換にするのは望ましくない、との事
  • 互換性を保ちつつ対応する場合は to_datetime に使用するタイムゾーンを指定するようにするのはどうか、と提案されている

[Bug #18018] Float#floor / truncate sometimes result that is too small.

p 291.4.floor(1) # => 291.4 (ok)
p 291.4.floor(2) # => 291.39 (not ok)
p 291.4.floor(3) # => 291.4 (ok)
p 291.4.floor(4) # => 291.4 (ok)
p 291.4.floor(5) # => 291.39999 (not ok)
p 291.4.floor(6) # => 291.4 (ok)

[Bug #18044] unusual behavior with Arabic string in a Hash

  • アラビア語の文字列を含む Hash でキーを検索すると常に nil が返ってくる、というバグ報告
foo = {"arabic" => "ٱلتَّوْبَة"}
p foo.keys # => ["arabic"]
p foo["arabic"] # => nil
p foo.values_at("arabic") => [nil]
foo.fetch "arabic" # Raises error with - did you mean "arabic" ?
  • どうして…、と思ったら上記の再現コードのキーに U200e が紛れているのが原因だったらしい
    • U200e は Left-to-Right Mark (LRM) と呼ばれる制御文字
  • 実際に Vim でコードを貼り付けるとこんな感じ

  • 制御文字を取り除いたら問題なく動作した
foo = {"arabic" => "ٱلتَّوْبَة"}
p foo.keys # => ["arabic"]
p foo["arabic"] # => "ٱلتَّوْبَة"
p foo.values_at("arabic") => {["ٱلتَّوْبَة"]=>[nil]}

[Feature #18040] Why should foo(1 if true) be an error?

  • foo(1 if true) がエラーになるのはなぜか?というバグ報告
  • パーサ的には 1 if true は式ではなくてステートメントなので現状は意図する挙動だとコメントされている
  • その後は 1 if true が式なのかステートメントなのかで議論が続いている
  • 最終的には『現実的に修正できない』という理由で Reject されている
  • ちなみに p foo((1 if true)) みたいに () でくくると動作する

[Feature #15211] Integer.try_convert

Regexp.try_convert(/re/)      # => /re/
# 失敗したら nil を返す
Regexp.try_convert("re")      # => nil
  • Integer.try_convert は最新版では実装済
p Integer.try_convert(10)
# => 10
p Integer.try_convert("10")
# => nil

# ちなみに内部で to_int を呼び出しているので Float だとこうなる
p Integer.try_convert(1.23)
# => 1

[Feature #18008] keyword_init? method for Struct

  • Struct.newkeyword_init: true したかどうかを判定するメソッドを追加する提案
  • 次のように Struct.new の戻り値に対して判定できる
S1 = Struct.new(:a, :b)
S2 = Struct.new(:a, :b, keyword_init: true)
S3 = Struct.new(:a, :b, keyword_init: false)

# 指定しなかった場合は nil を返す
pp S1.keyword_init?  # => nil
pp S2.keyword_init?  # => true
pp S3.keyword_init?  # => false
  • これは既に最新版で対応済み

[Feature #17724] Make the pin operator support instance/class/global variables

  • 元々はパターンマッチで ^@n を使用すると『ローカル変数かメソッドを期待する』とエラーメッセージになっていた
# ローカル変数かメソッドを期待する、というエラーメッセージが出力される
case 10
in ^@n
end
# => error: syntax error, unexpected instance variable, expecting local variable or method
# in ^@n
#     ^~
  • しかし、メソッドの場合でもエラーになるのでエラーメッセージを修正しよう、という内容のチケットだった
def n = 10

# メソッドを使用してもエラーになる
case 10
in ^n
end
# => error: n: no such local variable
  • これの対応として ^インスタンス/クラス/グローバル変数 が使えるように対応された
class X
  @a  = 1
  @@b = 2
  $c  = 3

  # インスタンス、クラス、グローバル変数が使える
  case 1
  in ^@a
  in ^@@b
  in ^$c
  end
end

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

今週は TracePoint 周りのバグがありました。

[Feature #17039] Remove Time#succ

  • Time#succ を削除するチケット
  • 1.9.2 の頃から廃止の警告がでてたらしい
    • warning: Time#succ is obsolete; use time + 1
  • と、いうわけで Ruby 3.1 から削除されるので使ってる場合は注意

[Bug #17945] Date::Infinity comparison <=> with Float::INFINITY not symmetric

  • Date::Infinity <=> Float::INFINITY が対称でないというバグ報告
require 'date'

p Float::INFINITY <=> Date::Infinity.new
# => 0
p Date::Infinity.new <=> Float::INFINITY
# => 1

[Bug #18031] Nested TracePoint#enable with target crashes

  • 以下のように TracePoint がネストしているとクラッシュするというバグ報告
    • どうして…
one = TracePoint.new(:call) {}
two = TracePoint.new(:call) {}

obj = Object.new
obj.define_singleton_method(:foo) {} # a bmethod

foo = obj.method(:foo)
# ここでクラッシュする
one.enable(target: foo) do
  two.enable(target: foo) {}
end
# コード例
loop do
 tp = TracePoint.new(:call){}
 tp.enable
 tp.disable
end
  • もう1つは複数の TracePoint を使うと2つ目の TracePoint が1つ目の TracePoint を上書きするらしい

[PR #4638] Fix infinite loop when b_return TracePoint throws

  • 次のように TracePointb_return 内で例外が発生すると無限ループになるバグの修正スクリプト
    • どうして…どうして…
class Foo
  define_singleton_method(:foo) { return }
end

TracePoint.trace(:b_return) do |tp|
  p tp
  raise
end

Foo.foo
  • 6日前に PR が立ってるけど特に進展がなさそう

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