2022/01/14 今回の気になった bugs.ruby のチケット

今週はレシーバに定義されている定数名とその値の Hash を返すメソッドの提案がありました。

[Bug #18475] Yielding an element for Enumerator in another thread dumps core

  • 以下のコードを実行すると segv するというバグ報告
def run
  Thread.new do
    1.times do |value|
      yield "some-value"
    end
  end.join
end

to_enum(:run).first
  • #18474 を調べている時に見つけたバグらしい

[Feature #18478] Module#constant_pairs

  • 定数名と定数の値の両方を返す Module#constant_pairs メソッドを追加する提案
module A
  B = 1

  class C
  end
end

A.constant_pairs # => { B: 1, C: A::C }
  • 現状だと以下のようなコードを書く必要がある
module A
  B = 1

  class C
  end
end

p A.constants.to_h { |c| [ c, A.const_get(c)] }
# => {:C=>A::C, :B=>1}

[Feature #10829] Add to_proc method to the Array class

  • 以下のような Array#to_proc を追加する提案
    • 7年前のチケット
class Array
  def to_proc
    proc { |receiver| receiver.send *self }
  end
end

# 1.send(:+, 3) のような呼び出しを行う
p [1, 2, 3, 4, 5].map(&[:+, 3])
# => [4, 5, 6, 7, 8]
  • 最近チケットが更新されていたんですがどこかで話題になってたんですかね?

Ruby の steep を試してみたメモ

雑な覚書

インストール

$ gem install steep
$ steep --version
0.47.0

注意点

$ steep --version
/home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.1/lib/active_support/tagged_logging.rb:60:in `current_tags': uninitialized constant ActiveSupport::TaggedLogging::Formatter::IsolatedExecutionState (NameError)

        IsolatedExecutionState[thread_key] ||= []
        ^^^^^^^^^^^^^^^^^^^^^^
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.1/lib/active_support/tagged_logging.rb:45:in `push_tags'
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.1/lib/active_support/tagged_logging.rb:95:in `push_tags'
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/steep-0.47.0/lib/steep.rb:146:in `block in new_logger'
    from <internal:kernel>:90:in `tap'
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/steep-0.47.0/lib/steep.rb:145:in `new_logger'
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/steep-0.47.0/lib/steep.rb:158:in `log_output='
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/steep-0.47.0/lib/steep.rb:162:in `<module:Steep>'
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/steep-0.47.0/lib/steep.rb:139:in `<top (required)>'
    from <internal:/home/worker/.rbenv/versions/3.1.0/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
    from <internal:/home/worker/.rbenv/versions/3.1.0/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
    from /home/worker/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/steep-0.47.0/exe/steep:7:in `<top (required)>'
    from /home/worker/.rbenv/versions/3.1.0/bin/steep:25:in `load'
    from /home/worker/.rbenv/versions/3.1.0/bin/steep:25:in `<main>'

lsp-vim + vim-lsp-settings で steep を使う

  • steep をインストール
" インストール
:LspInstallServer steep
  • steep のみを使うように設定
let g:lsp_settings_filetype_ruby = ['steep']
" solargraph と併用する場合はこんな感じ?
" let g:lsp_settings_filetype_ruby = ['solargraph', 'steep']
  • lsp-vim だと『グローバル』にインストールされている steep を使うので注意

サンプル環境

  • steep の設定ファル
    • 参照する Ruby のコードや rbs ファイルのパスなどを設定する
# ./Steepfile
target :app do
  check "lib"
  signature "sig"

  library "set", "pathname"
end
# ./lib/steep_example.rb
homu.name
  attr_reader :contacts

  def initialize(name:)
    @name = name
    @contacts = []
  end

  def guess_country()
    contacts.map do |contact|
      case contact
      when Phone
        contact.country
      end
    end.compact.first
  end
end

class Email
  attr_reader :address

  def initialize(address:)
    @address = address
  end

  def ==(other)
    other.is_a?(self.class) && other.address == address
  end

  def hash
    self.class.hash ^ address.hash
  end
end

class Phone
  attr_reader :country, :number

  def initialize(country:, number:)
    @country = country
    @number = number
  end

  def ==(other)
    if other.is_a?(Phone)
      other.country == country && other.number == number
    end
  end

  def hash
    self.class.hash ^ country.hash ^ number.hash
  end
end
  • Ruby の型情報の rbs ファイル
# ./lib/steep_example.rbs
class Person
  @name: String
  @contacts: Array[Email | Phone]

  def initialize: (name: String) -> untyped
  def name: -> String
  def contacts: -> Array[Email | Phone]
  def guess_country: -> (String | nil)
end

class Email
  @address: String

  def initialize: (address: String) -> untyped
  def address: -> String
end

class Phone
  @country: String
  @number: String

  def initialize: (country: String, number: String) -> untyped
  def country: -> String
  def number: -> String
  def ==: (Phone) -> (bool | nil)

  def self.countries: -> Hash[String, String]
end

動作

所感

  • コード補完やドキュメントの表示ができて便利
  • steep check で型チェックもできた
  • 一方で定義ジャンプで利用したかったが定義ジャンプは rbs ファイルの方を参照しているようで残念
    • これは回避方法あるんだろうか…

参照リンク

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

あけましておめでとうございます。
今年も引き続き書いていきたいと思います。
今週は Refinement 周りの便利メソッドが追加されたり匿名な * ** 引数をフォワードする機能がマージされました。

[Feature #18460] implicit self for .() syntax without rvalue

  • self を付けないで .() を呼び出せるようにする提案
m = 1.method(:+)
# 1.+(3) を呼びだす
m.(2) # 3

# self.() で呼び出せる
m.instance_exec { self.(2) }

# self なしで .() で呼び出せるようにしたい
m.instance_exec { .(2) }

[Feature #18459] IRB autocomplete dropdown colour options

  • Ruby 3.1 で入った irb の自動補完のダイアログの色をユーザ側で制御したいという提案
IRB.conf[:AUTOCOMPLETE] = {
  BG_COLOR: 0,
  FG_COLOR: 15,
}
  • 最近の irb だと色々とハイライトされるようになっているので細かいハイライトが制御できるようになるとよいんですかね?

[Bug #18294] error when parsing regexp comment

# これは OK
_re = /
  foo  # これはコメント
/x

# コメントにエスケープされた文字があるとエラーになる
_re = /
  foo  # \M-ca
/x
# /tmp/vxUTJfT/17:8: Invalid escape character syntax
#   foo  # \M
#          ^~
/ C:\\[a-z]{5} # e.g. C:\users /x
# =>                      ^
# => invalid Unicode escape (SyntaxError)

[Feature #12737] Module#defined_refinements

  • レシーバで定義されている refine されたオブジェクトとその Refinement オブジェクトの Hash を返す Module#defined_refinements メソッドを追加する提案
module M
  refine String do
    $M_String = self
  end

  refine Integer do
    $M_Integer = self
  end
end

p M.defined_refinements #=> {String => $M_String, Integer => $M_Integer}
  • 最終的にモジュール内で定義されている Refinement オブジェクトを返す Module#refinementsrefine されているクラスを取得する Refinement#refined_class の2つのメソッドが追加された
module Ex
  refine Integer do
    # ...
  end

  refine String do
    # ...
  end
end

pp Ex.refinements
# => [#<refinement:Integer@Ex>, #<refinement:String@Ex>]

# refine されているクラスを返す
pp Ex.refinements.first.refined_class
# => Integer

[Bug #18441] Fix inconsistent parentheses with anonymous block forwarding

  • Ruby ではメソッド定義やメソッド呼び出しの () は省略する事ができる
def demo positional, &block
  other positional, &block
end
  • しかし、次のように Ruby 3.1 で追加された匿名のブロック引数を受け取ったり渡したりするとエラーになる
# syntax error, unexpected local variable or method, expecting ';' or '\n'
# other positional, &
#       ^~~~~~~~~~
def demo positional, &
  other positional, &
end
  • これが一貫性がいないというバグ報告
  • これは Ruby& の後にブロック変数名を探索するので
def a &
  b
  b
end
  • が、次のように解釈される為
def a(&b)
  b
end
  • なので
def demo positional, &
  other positional, &
end
def demo(positional, &other positional, &)
end
  • とパースされるらしい
  • ちなみに ; を付けると () を付けなくても呼び出せる
def demo positional, &;
  other positional, &
end

def demo positional, &;
  other positional, &;
  another positional, &
end
  • このチケットは Reject されている

deprecated な機能が削除されてる

マージされた機能

def foo(*)
  bar(*)

  # こう書くこともできる
  piyo(*, *)
end

def baz(**)
  quux(**)
end

2021年を振り返って

去年に引き続いて技術的な話ではなくて今年読んで面白かったマンガの紹介をします。
ちなみに今年は kindle で880冊読みました。
1日に2冊以上読んでる計算になるけどそんなに読んでる気はしないんですがねえ。

宝石の国

宝石の国(1) (アフタヌーンコミックス)

宝石を擬人化したかわいいキャラクターが登場するマンガ……かと思いきや思いっきりシリアスで過酷な内容のマンガです。
アレがアレになってああなってしまう、ぐらいの前情報ぐらいしかなかったんですが実際読んでみると思ったよりも重かった。
彼は最終的にどうなってしまうんですかねえ…境遇が可愛そうすぎるので最終的には幸せになってほしいが…。

ポケットモンスタースペシャル(15) ~ (22)

ポケットモンスタースペシャル(15) (てんとう虫コミックススペシャル)

知り合いからおすすめされて一部だけ読みました。
ちょうどルビサファの部分なんですが原作やってないんだよねえ。
なので正直ポケモンとか登場人物とかはそこまで思い入れはなかったんですが内容は王道かつめっちゃ熱い展開でよかった…クールなのに実は熱血系主人公いいよね…。

呪術廻戦

呪術廻戦【期間限定無料】 1 (ジャンプコミックスDIGITAL)

今年流行ってた奴。
アニメはちょろっとしか見てないんですが原作は全部読みました。
容赦なく人が死にまくっててやばいわお

呪術廻戦 0 東京都立呪術高等専門学校

今映画やってるやつ。
里香ちゃんかわいいよ里香ちゃん。
突き抜けたヤンデレいいよね…(ヤンデレとはちょっと違うが…。
早く映画見に行きたい。

3月のライオン

3月のライオン 1 (ジェッツコミックス)

将棋マンガの皮をかぶったラブコメです。
最初は将棋メインだったのに気づいたらラブコメになっていたんだぜハチクロとは違って安心して読めるんだぜ。
途中から主人公が吹っ切れてハチャメチャになっていく展開が好き。

ドキュンサーガ

ドキュンサーガ 1 (MFC)

Web マンガがコミカライズされたやつです。
序盤は結構修正されているけど内容自体はほとんど Web 版と同じ。
コミカライズの帯にも書かれていたけど本番は2巻からなので興味があるなら2巻まで込みで読んでもらいたい。
世界観とか設定がよー考えられとるなあ。

カラオケ行こ!

カラオケ行こ! (ビームコミックス)

ヤクザと高校生がカラオケに行くマンガです。
女の園の星』と同じ作者なんですが相変わらずシュールで面白い。
何も考えないで読めるので箸休めにちょうどいいですねえ。

ドラゴンクエスト ダイの大冒険 勇者アバンと獄炎の魔王

ドラゴンクエスト ダイの大冒険 勇者アバンと獄炎の魔王 1 (ジャンプコミックスDIGITAL)

この時代にダイ大の新作が読めるとは…原作に思い入れが強いので読んでて楽しいですね。マトリフの登場回とか激アツじゃあ…。
そろそろダイ大の続編もやってくれんかのぉ。

伊藤潤二の猫日記 よん&むー

伊藤潤二の猫日記 よん&むー

飼い猫のことを書いているエッセイマンガなのに全然そんな感じがしないのが伊藤潤二のなせる技。
伊藤潤二が猫のことを書いたらこうなるだろうなあ、というのが想像できて面白い。

天才ファミリー・カンパニー

天才ファミリー・カンパニー (1) (幻冬舎コミックス漫画文庫)

最初はちょっと苦手なマンガだなあ、と思いながら読んでいたけど読み進めていくとどんどん引き込まれていって面白かった。
内容とは直接関係ないけどインターネット通販の先駆けとか時代を感じますねえ。

東京卍リベンジャーズ

東京卍リベンジャーズ(1) (週刊少年マガジンコミックス)

今年流行ってた奴。
読む前は正直そんなに期待してなかったんですが普通に面白かったです。
タイムリープしてヤンキーになるのが新鮮だった。
関係ないけどこの作者って新宿スワンを書いてた人なんですね。
読者層が全然違っててびっくりした。
最後はハッピーエンドになってほしいなあ…。

魍魎の匣

魍魎の匣(1) (カドカワデジタルコミックス)

全然時事ものじゃないんですが前から気になっててやっと読みました。
いやーこれは全部一気読みするのが絶対にいいですね。
読み終わった後に時系列を見てニヤニヤしました。

何度、時をくりかえしても本能寺が燃えるんじゃが!?

何度、時をくりかえしても本能寺が燃えるんじゃが!? コミック 1-4巻セット

織田信長タイムリープして本能寺の変を回避するマンガです。
設定が既に面白い。
内容がぶっ飛びまくっててよいですね。

来世は他人がいい

来世は他人がいい(1) (アフタヌーンコミックス)

何気なく買ってみたら思った以上に主人公がぶっ飛んでてビビった。
内容がバイオレンスすぎる…。

14歳(フォーティーン)

14歳(フォーティーン)(1) (ビッグコミックス)

もうなんというかすごいとしか言いようがない。
楳図かずおむずかしいなあ。

風都探偵

風都探偵(1) (ビッグコミックス)

仮面ライダーW の正式な続編。
脚本も特撮と同じなので本当に特撮からの延長線上で話が進んでいてめっちゃいい…。
今年アニメ化もするのでめちゃくちゃ楽しみですね。
関係ないけど1巻ごとにちゃんと話が区切られているので読んでる側としては読みやすいですね。

ダーウィン事変

ダーウィン事変(1) (アフタヌーンコミックス)

どっちかって言うと動物愛護とか進化論的な話なのかと思ったら内容は完全に菜食主義者がやべーやつっていう内容だった。
まあ何事もやり過ぎはよくないよね。
3巻で一区切りって感じなので続きが気になる。

ジョジョリオン

ジョジョの奇妙な冒険 第8部 モノクロ版 1 (ジャンプコミックスDIGITAL)

完結したのでやっとこさ一気読みしました。
世間の評価的には結構厳し目なんですが個人的にはところどころ4部をオマージュしている部分があって面白かった。
このあたりはリアルタイムで読んでいたのと一気読みしたので印象が違いそう。

ウマ娘 シンデレラグレイ

ウマ娘 シンデレラグレイ 1 (ヤングジャンプコミックスDIGITAL)

ゲームもアニメも見ていないんですがやっとこさコミカライズだけ手を出しました。
競馬は全然分からんけどスポ根ものと読めば普通に面白いですね。
5巻のラストのオグリキャップタマモクロスは激アツだった…。
アニメもそろそろ見ないとなあ。

ベルセルク

ベルセルク 1 (ヤングアニマルコミックス)

最近半額だったので全部買って一気に読んだ。
なんで作者亡くなってしもたんや…最後まで読んでみたかった…。

と、言うことで今年読んで気になったマンガをざっと書いてみました。
ここでは全然書ききれず他にも

  • チ。―地球の運動について―
    • 天動説から地動説へと移り変わる話、グロい
  • 葬送のフリーレン
    • 勇者が魔王を倒した後の物語
  • ダンダダン
    • ターボババアやアクロバティックさらさらがでてくる斬新なマンガ
  • 神クズ☆アイドル
    • クズな男性アイドルに神な女性アイドルが憑依するマンガ
  • 【推しの子】
    • 推しの子に転生するマンガ、今の所は演劇もの
  • かげきしょうじょ!!
    • いわゆる宝塚的な学校が舞台のマンガ、演劇もの
  • 蜘蛛ですが、なにか?
    • 転生したら蜘蛛だった
  • 世界の終わりに柴犬と
    • 終末世界で柴犬と旅するマンガ
  • あんじゅう
  • パパと親父のウチご飯
  • 魔法使いの嫁
  • ゲーミングお嬢様
  • おとなりに銀河
  • マッシュル-MASHLE-
  • 海が走るエンドロール
  • メタモルフォーゼの縁側
  • 東京入星管理局
  • さくら江さんはグイグイ来すぎる。
  • etc...

あたりが面白かったです。
それではよいお年を


以下、今年新しく読んだ(買った)マンガ。

Ruby 3.1 がリリースされた!

今年も無事にクリスマスに Ruby がリリースされました!

Ruby 3.1 で追加される機能などは以下のスライドで紹介しているので気になる人は読んでみてください。

Ruby 3.1 へ移行するときの注意点

Ruby 3.1 で非互換になった機能があるのでいくつか紹介します。
これ以外にも細かい変更点はあるので詳しくは NEWS などを参照してください。

YAML.load が非互換になる

Ruby 3.1 では YAML の実装である Psych のバージョンが 4.0.0 にメジャーアップデートされます。
この影響により YAML.load など一部の機能が非互換になります。
例えば YAML.loadYAML.safe_load に置き換わったので次のようにエイリアスを使用している YAML ファイルは YAML.load では読み込めなくなります。

require "yaml"

data = <<~EOS
default: &default
  aaa: aaa
development:
  <<: *default
EOS

# Ruby 3.0 => OK 読み込める
# Ruby 3.1 => NG 読み込めない
pp YAML.load(data)

これを回避する方法として YAML.unsafe_load を使用する方法があります。

# OK
# .unsafe_load を使用すると Ruby 3.1 でも読み込めるようになる
pp YAML.unsafe_load(data)

読み込んでいる YAML ファイルによっては動かなくなる可能性があるので注意しましょう。
他にも非互換になった箇所があるので詳しくは以下の記事を参照してください。

Class#descendants が Ruby 3.1 には入らなかった

Ruby 3.1 には Class#descendants という新しいメソッドが入る予定だったんですが直前で Revert されました。
そもそも入ってないので基本的に既存のコードには影響はないのですが Rails 7.0.0 がこの Class#descendants に依存している関係で Rails 7.0.0 と Ruby 3.1 の組み合わせだと動かない可能性があるので注意する必要があります。
この問題は Rails 7.0.1 (仮) で対応される予定です。

Module#prepend の挙動が調整

『既に継承リストに存在しているモジュールを prepend した』ときの挙動が調整されました。
これによって以下のように Ruby 2.7 ~ 3.1 間で全ての挙動が異なるのでちょっとだけ気にしておく必要があります。

module M1; end
module M2; end
class A; include M2; end

M2.prepend M1
A.prepend M1

p A.ancestors
# 2.7 => [M1, A, M2, Object, Kernel, BasicObject]
# 3.0 => [A, M1, M2, Object, Kernel, BasicObject]
# 3.1 => [M1, A, M1, M2, Object, Kernel, BasicObject]

refine 内での include / prepend が非推奨になった

refine 内での include / prepend が非推奨になりました。
-W を付けて Ruby を実行すると警告が出るようになります。

using Module.new {
  module Twice
    def twice
      self + self
    end
  end

  refine String do
    # warning: Refinement#include is deprecated and will be removed in Ruby 3.2
    include Twice
  end

  refine Integer do
    # warning: Refinement#include is deprecated and will be removed in Ruby 3.2
    include Twice
  end
}

pp "homu".twice
pp 42.twice

これの代替として import_methods という機能が新しく追加されたので今後は import_methods を使っていく必要があります。

# 使い方は一緒
using Module.new {
  module Twice
    def twice
      self + self
    end
  end

  refine String do
    # no warning
    import_methods Twice
  end

  refine Integer do
    # no warning
    import_methods Twice
  end
}

pp "homu".twice
pp 42.twice

foo[0], bar.baz = a, b の評価順が変更

多重代入を行った際の評価順が変わりました。
以下のようなコードは

foo[0], bar.baz = a, b

Ruby 3.0 では以下の評価順です。

  1. a が評価される
  2. b が評価される
  3. foo が評価される
  4. foo[]= が評価される( foo[0]= )
  5. bar が評価される
  6. barbaz= が評価される( bar.baz=

Ruby 3.1 では以下のような評価順になります。

  1. foo が評価される
  2. bar が評価される
  3. a が評価される
  4. b が評価される
  5. foo[]= が評価される( foo[0]= )
  6. barbaz= が評価される( bar.baz=

【一人 bugs.ruby Advent Calendar 2021】番外編: 今年みた Ruby のバグ報告【25日目】

一人 bugs.ruby Advent Calendar 2021 25日目の記事になります。
今日で Advent Calendar も最後という事で今回は今年みた Ruby のバグをいくつか紹介してみようと思います。
またこれから紹介する修正済みのバグは Ruby 3.1 ではなくて古い Ruby でもバックポートされている可能性があるので注意してください(〜で修正済みと書かれていても RUby 2.7.x 系でバックポートされていて修正済みの可能性があります。

[Bug #18377] Integer#times has different behavior depending on the size of the integer

Integer#+ を書き換えると特定の値で Integer#times の挙動に影響を与えるというバグ報告です。

# これは問題がない
(2**1).times do
  Integer.undef_method(:+)
  Integer.define_method(:+) do |_other|
    puts "my custom add"
  end
end

# FIXNUM を越える値に対して `times` を呼び出すと Integer#+ を呼び出してエラーになる
# `times': undefined method `<' for nil:NilClass (NoMethodError)
(2**65).times do
  Integer.undef_method(:+)
  Integer.define_method(:+) do |_other|
    # ここが呼び出されるようになる
    puts "my custom add"
  end
end

内部で Integer#+ を呼び出すようになっていたところを呼び出さないようにして対応済みです。

[Bug #17675] StringIO#each_byte doesn't check for readabilty while iterating

IO がクローズしているのにイテレーションが処理されてしまうというバグ報告です。

require "stringio"

strio = StringIO.new("1234")
strio.each_byte do |byte|
  puts byte
  # ここでクローズしているがイテレーションは引き続き処理されている
  strio.close
end
# => 49
#    50
#    51
#    52

この問題は修正済みで最新版では IOError が発生するようになります。

require "stringio"

strio = StringIO.new("1234")
strio.each_byte do |byte|
  puts byte
  strio.close
end
# => 49
#    error: `each_byte': not opened for reading (IOError)

[Bug #18343] empty hash passed to Array#pack causes Segmentation fault (2.6)

Ruby 2.4 ~ 2.6 で Array#pack に空の Hash を渡すと Segmentation fault が発生するというバグ報告です。

# これで segv する
[0].pack('c', {})

この問題は Ruby 2.7 以降では再現せず Ruby 2.6 は現状セキリュティサポートのみなのでこのチケットは閉じられています。

[Bug #18292] 3.1.0-dev include cause Module to be marked as initialized

これは Ruby 3.1.0-dev で発生したバグになります。
次のように Module を継承して include した後に super を呼ぶとエラーになってしまうバグです。

class Mod1 < Module
  def initialize(...)
    super
  end
end
p Mod1.new
# => #<Mod1:0x000055b6dc5a5d00>

class Mod2 < Module
  def initialize(...)
    include Enumerable
    super
  end
end
p Mod2.new
# 3.0.2     => #<Mod2:0x000055b6dc5a59e0>
# 3.1.0-dev => error: `initialize': already initialized module (TypeError)

以下のコードだけでもエラーになったので include 後に Module#initialize を呼ぶとダメなのかも?

class Mod2 < Module
  def initialize(...)
    include Enumerable
    super
  end
end
p Mod2.new
# 3.0.2     => #<Mod2:0x000055b6dc5a59e0>
# 3.1.0-dev => error: `initialize': already initialized module (TypeError)

これはまだ最新の 3.1.0-dev でも再現していたので Ruby 3.1 でも残ったままになってしまっているかも。

[Bug #18329] Calling super to non-existent method dumps core

存在しない super を呼び出すとコアダンプするというバグ報告です。
次のコードを Ruby 3.0.2 で実行すると segv します。

module Probes
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def probe(*methods)
      prepend(probing_module(methods))
    end

    def probing_module(methods)
      Module.new do
        methods.each do |method|
          define_method(method) do |*args, **kwargs, &block|
            super(*args, **kwargs, &block)
          end
        end
      end
    end
  end
end

class Probed
  include Probes

  probe :danger!, :missing

  def danger!
    raise "BOOM"
  end
end

5.times do
  subject = Probed.new
  subject.danger! rescue RuntimeError
  subject.missing rescue NoMethodError
end

ちょっと分かりづらいので最小構成にすると以下のような感じです。

class Probed
  def self.probing_module(methods)
    Module.new do
      methods.each do |method|
        define_method(method) do |*args, **kwargs, &block|
          super(*args, **kwargs, &block)
        end
      end
    end
  end

  prepend probing_module [:danger!, :missing]

  def danger!
  end
end

subject = Probed.new
subject.danger!

# ここで存在しない super を呼び出している
subject.missing rescue NoMethodError

# ここで segv
subject.danger!

この問題は既に修正されていて Ruby 3.0.3 にもバックポートされています。

[Bug #18243] Ractor.make_shareable does not freeze the receiver of a Proc but allows accessing ivars of it

次のように Ractor 内で別の Ractor のオブジェクトが書き換えられてしまうバグ報告です。

class C
  attr_accessor :foo
  def setter_proc
    Ractor.make_shareable(proc {})
#     Ractor.make_shareable(-> v { @foo = v })
  end
end

c = C.new
c.foo = 1
p c
# => #<C:0x0000559bf1df5880 @foo=1>

# インスタンス変数を書き換える proc を生成
# c 自体は freeze されてない
proc = c.setter_proc
p c.frozen?
# => false

# Ractor 内で setter_proc を呼び出すと c のオブジェクトが書き換えら得てしまう
# これがバグ
Ractor.new(proc) { |s| s.call(42) }.take
p c
# => #<C:0x0000559bf1df5880 @foo=42>

この問題は修正済みで生成 Procself が共有可能オブジェクトかどうかまで参照するようになりました。

# OK: self は参照可能オブジェクトである
Ractor.make_shareable(nil.instance_eval { -> {} })

# NG: self は参照可能オブジェクトではない
# error: `make_shareable': Proc's self is not shareable: #<Proc:0x00007fb6e26e5460 /tmp/vDlYFAi/48:5 (lambda)> (Ractor::IsolationError)
Ractor.make_shareable(-> {})

[Bug #17719] Irregular evaluation order in hash literals

Hash リテラルで同名のキーが存在する場合に評価順が左からにならないバグ報告です。
これは Ruby 3.1 で修正されました。

ary = []
{ a: ary << 1, b: ary << 2, a: ary << 3 }
pp ary
# Ruby 3.0 => [1, 3, 2]
# Ruby 3.1 => [1, 2, 3]

[Bug #1823] Conversion to float not working for object with to_f method

Thread#join で引数を Float に変換しているが #to_f が呼ばれていないというバグ報告です。
Ruby 3.0 以降から再現するようになりました。

class Something
  def to_f
    0.1
  end
end

# error: `join': can't convert Something into Float (TypeError)
Thread.new{ }.join(Something.new)

この問題は Ruby 3.1 で修正済みです。

[Bug #18180] opt_newarray_min/max instructions ignore refined methods

以下のようなケースで Refinements が正しく反映されていないというバグ報告です。

module M
  refine Array do
    def min; :min; end
    def max; :max; end
  end
end

using M

# これは Refinements が適用される
pp [1, 2, 3].min    # => :min

# これは Refinements が適用されない
pp [1+0, 2, 3].min  # => 1

これはレシーバがリテラルの場合に最適化を行っていて Refinements が反映されなくなってしまっているが原因らしいです。
これは Ruby 3.1 で修正済みです。

module M
  refine Array do
    def min; :min; end
    def max; :max; end
  end
end

using M

pp [1+0, 2, 3].min
# Ruby 3.0 => 1
# Ruby 3.1 => :min

[Bug #17048] Calling initialize_copy on live modules leads to crashes

以下のコードで Ruby がクラッシュするというバグ報告です。

loop do
  m = Module.new do
    prepend Module.new
    def hello
    end
  end

  klass = Class.new { include m }
  m.send(:initialize_copy, Module.new)
  GC.start

  klass.new.hello rescue nil
end

上記のように Module#initialize_copy を呼び出すとクラッシュする可能性があるらしいです。
Ruby 3.1 では Module#initialize_copy を呼び出すと TypeError が発生するように修正されました。

module A
end

# error: `initialize_copy': already initialized module (TypeError)
A.send(:initialize_copy, Module.new) # fine, no one inherits from A

[Bug #18160] IndexError raised from MatchData#{offset,begin,end} does not keep the encoding of the argument

MatchData#{offset,begin,end} で発生した IndexErrorエンコーディングを保持してないバグ報告です。

pp RUBY_VERSION  # => "3.0.2"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: \xE3\x81\x82"

これは修正されて Ruby 3.0.3 以降では意図する文字コードで出力されます。

pp RUBY_VERSION  # => "3.0.3"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: あ"

[Bug #18084] JSON.dump can crash VM.

次のように再帰的な HashJSON.dump に渡すと VM がクラッシュするバグ報告です。

require 'json'

x = {}
# 自身に自身を割り当てる
x[:x] = x

# machine stack overflow in critical region (fatal)
p JSON.dump(x)

これは Ruby 2.7 から再現しており、Ruby 2.7 以前は SystemStackError が発生していました。

require 'json'

x = {}
x[:x] = x

# Ruby 2.6 の場合
# error: stack level too deep (SystemStackError)
p JSON.dump(x)

これは修正されて Ruby 3.1 からは SystemStackError が発生します。

[Bug #18080] Syntax error on one-line pattern matching

次のように『カッコで囲まれていないパラメータを持つメソッドの戻り値を右代入で使用するとシンタックスエラーになる』というバグ報告です。

# パラメータがなかったり、カッコが付いている場合は OK
p do
end => a
p a #=> nil

p(1) do
end => a
p a #=> 1

# カッコがないパラメータがある場合はシンタックスエラーになる
p 1 do
end => a
#=>
# syntax error, unexpected =>, expecting end-of-input
# end => a
#    ^~

# これは1行 in でも同様にシンタックスエラーになる
p 1 do
end in a
#=>
# syntax error, unexpected `in', expecting end-of-input
# end in a
#     ^~

このエラーは意図的ではないが修正するのは難しいらしいです。

[Bug #18053] Crashes and infinite loops when generating partial backtraces in Ruby 3.0+

以下のコードを Ruby 3.0 以降で実行すると segv するというバグ報告です。

def foo
  caller_locations(2, 1).inspect # this will segv
  # caller_locations(2, 1)[0].path # this will infinite loop
end

1.times.map { 1.times.map { foo } }

これは Ruby 3.0 の最適化のバグらしいです。
Ruby 3.0.3 以降では修正済みです。

[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

修正 PR は既にあるんですが、いくつか問題がありまだマージされていません。

[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
__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

この問題は Ruby 3.1 で改善されています。

[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=>[]}

TracePoint#parameters だけではなくて define_method + TracePoint 全般の問題らしいです。

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] になるべき?

この問題は Ruby 3.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倍)

ちなみに Integer#digits は各桁を配列として返すメソッドになります。

pp 16.digits    # => [6, 1]
pp 1234.digits  # => [4, 3, 2, 1]

この問題は Ruby 3.1 で修正済みです。

[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

このチケットがきっかけで Ruby 3.1 から以下のように挙動が変わりました。

[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

この問題は Ruby 3.1 で修正済みです。

[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

この問題は Ruby 3.0.2 で修正済みです。

[Bug #17857] when 0r and when 0i do not match with case 0

0r === 00i === 0true を返すが case-when でマッチしないというバグ報告です。

# これは true を返す
p 0r === 0  # => true
p 0i === 0  # => true

# しかし case-when では 0r などにマッチしない
case 0
when 0r
  p :hoge
when 0i
  p :foo
else
  p :bar
end
# 期待する挙動 => :hoge
# 実際の挙動   => :bar

これは最適化のバグらしく、最適化を無効にして実行すると問題なく動作します。

# 最適化を無効にして Ruby のコードを実行する
RubyVM::InstructionSequence.compile(<<END, specialized_instruction: false).eval
case 0
when 0r
  p :hoge
when 0i
  p :foo
else
  p :bar
end
# => :hoge
END

この問題は Ruby 3.1 で修正済みです。

[Bug #17814] inconsistent Array.zip behavior

以下のように Array#zip だとイテレーションが1回余計に呼ばれているというバグ報告です。

i = 0
# 1 ずつ増えるカウンタ
e = Enumerator.produce { i += 1 }

# 1つ余計にイテレーションが発生する
p [0, 0, 0, 0].zip e
# => [[0, 1], [0, 2], [0, 3], [0, 4]]
p i
# 期待する挙動 => 4
# 実際の挙動   => 5

# Enumerable#zip だと再現しない
p [0, 0, 0, 0].each.zip e
# => [[0, 6], [0, 7], [0, 8], [0, 9]]
p i
# => 9

Enumerable#zip だと問題ないので対応する場合はこっちを使うとよさそう。
この問題は Ruby 3.1 で修正済みです。

[Bug #4443] odd evaluation order in a multiple assignment

以下のように多重代入した時に先に右辺のメソッドが呼び出されるというバグ報告です。

def foo
  p :foo
  []
end
def bar
  p :bar
end

# bar -> foo という順に評価される
x, foo[0] = bar, 0
# output:
# :bar
# :foo

# これは foo -> bar という順になる
foo[0] = bar
# output:
# :foo
# :bar

10年前のチケットで Ruby 3.1 で修正されました。
Ruby 3.1 だと以下のような挙動になります。

def foo
  p :foo
  []
end
def bar
  p :bar
end

# Ruby 3.1だと foo -> bar と評価されるようになった
x, foo[0] = bar, 0
# output:
# :foo
# :bar

[Bug #17754] NoMethodError#to_s makes segmentation fault when Module#name returns non string value

以下のように .name が文字列以外を返した場合に SEGV するというバグ報告です。

class C
  def self.name
    42
    # これなら OK
    # "42"
  end
end
# C に対して NoMethodError なエラーが発生すると SEGV する
C.this_method_does_not_exist

これは Ruby 3.0.0 で再現し、Ruby 3.0.1 では修正済みです。

[Bug #17756] StringScanner#charpos makes segmentation fault when target.byteslice returns non string value

以下のように StringScanner を使用すると SEGV するというバグ報告です。

require 'strscan'
string = 'ruby'
scnanner = StringScanner.new(string)
pre = Module.new do
  def byteslice(*args)
  end
end
string.singleton_class.prepend(pre)
scnanner.charpos

この問題は Ruby 3.0.3 で修正済みです。

[Bug #17739] Array#sort! changes the order even if the receiver raises FrozenError in given block

Array#sort! のブロック内でレシーバを freeze すると例外が発生するがソート済みになっているというバグ報告です。

array = [1, 2, 3, 4, 5]
begin
  array.sort! do |a, b|
    array.freeze if a == 3
    1
  end
rescue => err
  # 例外が発生する
  p err #=> #<FrozenError: can't modify frozen Array: [5, 4, 3, 2, 1]>
end

# 例外が発生してもソート済みになっている
p array #=> [5, 4, 3, 2, 1]

ちなみに break した場合はそこまでのソートになっている

array = [1, 2, 3, 4, 5]
array.sort! do |a, b|
  break if a == 3
  1
end

# 途中までソートされた状態
p array #=> [3, 4, 2, 1, 5]

この問題は Ruby 3.1 で修正済みです。

[Bug #17719] Irregular evaluation order in hash literals

Hash リテラルでキーが重複している場合に以下のような評価順になるというバグ報告です。

{ foo: p(1), bar: p(2), foo: p(3) }
# => 1
#    3
#    2

Ruby では左から右に評価されるのが一般的なので Ruby 3.1 では左から右に向かって評価されるように修正されました。

{ foo: p(1), bar: p(2), foo: p(3) }
# => 1
#    2
#    3

[Bug #17652] GC compaction crash on mprotect

以下のコードを実行した時に GC compaction でクラッシュするというバグ報告です。

GC.auto_compact = true

times = 20_000_000
arr = Array.new(times)
times.times do |i|
  arr[i] = "#{i}"
end

arr = Array.new(1_000_000, 42)
GC.start

puts "ok"

この問題は Ruby 3.1 で修正済みです。

[Bug #17661] IO#each will segfault when if file is closed inside an each_byte block

以下のように File#each_byte 内で File#close すると segv するというバグ報告です。

file = File.open(__FILE__)
file.each_byte do |byte|
  file.close
end

この問題は Ruby 3.0.3 で修正済みです。

[Bug #17667] Module#name needs synchronization

Module#name は非同期処理に対応していないので以下のようにすると segv するというバグ報告です。

class C
  @iv = 1
end
Ractor.new {
  loop {
    C.name
  }
}

class C
  0.step { |i|
    instance_variable_set("@iv#{i}", i)
  }
end

この問題はまだ未修正のようです。

[Bug #17649] defined? invokes method once for each syntactic element around it

defined? の式で複数回メソッドが呼ばれることがあるというバグ報告です。
以下の例だと x メソッドが defined? 時に複数回呼ばれることがあるらしい。

public def x
  $times_called += 1
end

def times_called
  $times_called = 0
  yield
  $times_called
end

# without `defined?`
times_called { x }           # => 1
times_called { -x }          # => 1
times_called { --x }         # => 1
times_called { ---x }        # => 1
times_called { x+0+0 }       # => 1
times_called { x.pred.pred } # => 1
times_called { x.x.x.x.x.x } # => 6

# with `defined?`
times_called { defined? x }           # => 0
times_called { defined? -x }          # => 1
times_called { defined? --x }         # => 2
times_called { defined? ---x }        # => 3
times_called { defined? x+0+0 }       # => 2
times_called { defined? x.pred.pred } # => 2
times_called { defined? x.x.x.x.x.x } # => 15

これは defined? a.b.c.d を呼び出した時に a a.b a.b.c a.b.c.d が個別に呼び出されしまっているからのようです。

puts RubyVM::InstructionSequence.new("defined? a.b.c.d").disasm
__END__
output:
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,16)> (catch: TRUE)
== catch table
| catch type: rescue st: 0001 ed: 0039 sp: 0000 cont: 0041
| == disasm: #<ISeq:defined guard in <compiled>@<compiled>:0 (0,0)-(-1,-1)> (catch: FALSE)
| local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| [ 1] $!@0
| 0000 putnil
| 0001 leave
|------------------------------------------------------------------------
0000 putnil                                                           (   1)[Li]
0001 putself
0002 defined                                func, :a, false
0006 branchunless                           41
0008 putself
0009 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0011 defined                                method, :b, false
0015 branchunless                           41
0017 putself
0018 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0020 opt_send_without_block                 <calldata!mid:b, argc:0, ARGS_SIMPLE>
0022 defined                                method, :c, false
0026 branchunless                           41
0028 putself
0029 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0031 opt_send_without_block                 <calldata!mid:b, argc:0, ARGS_SIMPLE>
0033 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>
0035 defined                                method, :d, true
0039 swap
0040 pop
0041 leave

この問題は Ruby 3.1 で修正済みです。

public def x
  $times_called += 1
end

def times_called
  $times_called = 0
  yield
  $times_called
end

p times_called { defined? x.x.x.x.x.x }
# Ruby 3.0 => 15
# Ruby 3.1 => 5

[Bug #17590] M.prepend M has hidden side effect

M.prepend M を呼び出すとエラーになるが副作用があるというバグ報告です。
以下のように M.prepend M を呼んだ場合とそうでない場合で差異があります。

module M; end
class C; end
C.prepend M
C.include M

module M2; end
M2.prepend M
C.include M2

# M.prepend M を呼んでない場合
p C.ancestors # => [M, C, M2, Object, Kernel, BasicObject]


M.prepend M rescue nil
module M3; end
M3.prepend M
C.include M3

# M.prepend M を呼んだ場合
# M が複数追加されている…
p C.ancestors # => [M, C, M3, M, M3, M2, Object, Kernel, BasicObject]

これは Ruby 3.1 で修正済みです。

module M; end
class C; end
C.prepend M
C.include M

M.prepend M rescue nil
module M2; end
M2.prepend M
C.include M2

p C.ancestors
# Ruby 3.0 => [M, C, M2, M, M2, Object, Kernel, BasicObject]
# Ruby 3.1 => [M, C, M2, Object, Kernel, BasicObject]

また、上記とは別に以下のように M.prepend M すると M.ancestors # => [M, M] となる問題もあってこれも Ruby 3.1 では修正済みです。

module M
end

# 継承リストは自身だけ
pp M.ancestors
# => [M]

begin
  # 自身を prepend するとエラーになる
  # これは Ruby 2.7 でも 3.0 でも同じ
  M.prepend M
rescue => e
  puts "error : #{e.message}"
  # => error : cyclic prepend detected
end

# M.prepend M はエラーになるが Ruby 3.0 では副作用がある
# Ruby 3.1 では修正済み
pp M.ancestors
# Ruby 2.7 => [M]
# Ruby 3.0 => [M, M]
# Ruby 3.1 => [M]

[Bug #17554] [PATCH] Fix ObjectSpace.dump to include singleton class name

次のように ObjectSpace.dump に特異クラスを渡した場合に Ruby 3.0 だと "name" が含まれなくなっているというバグ報告です。

require "objspace"

puts ObjectSpace.dump(Object.new.singleton_class)
# 2.7 => {"address":"0x55adae76b630", "type":"CLASS", "class":"0x55adae7a76d0", "name":"Object", "references":["0x55adae7a9250", "0x55adae76b720"], "memsize":464, "flags":{"wb_protected":true}}
# 3.0 => {"address":"0x55d9048e80e0", "type":"CLASS", "class":"0x55d90476d738", "references":["0x55d90476e8b8", "0x55d9048e8158"], "memsize":472, "flags":{"wb_protected":true}}

これは Ruby 3.1 でまた別の情報を返すようにして対応されたようです。

require "objspace"

puts ObjectSpace.dump(Object.new.singleton_class)
# Ruby 2.7 => {"address":"0x55d5a0f891e0", "type":"CLASS", "class":"0x55d5a0ffb6f0", "name":"Object", "references":["0x55d5a1001258", "0x55d5a0f89348"], "memsize":464, "flags":{"wb_protected":true}}
# Ruby 3.0 => {"address":"0x55cfdc251690", "type":"CLASS", "class":"0x55cfdbf99718", "references":["0x55cfdbf9a898", "0x55cfdc251708"], "memsize":472, "flags":{"wb_protected":true}}
# Ruby 3.1 => {"address":"0x7f97065096b0", "type":"CLASS", "class":"0x7f9709a1a6b0", "superclass":"0x7f9709a1a868", "real_class_name":"Object", "singleton":true, "references":["0x7f9709a1a868", "0x7f9706509728"], "memsize":480, "flags":{"wb_protected":true}}

[Bug #17519] set_visibility fails when a prepended module and a refinement both exist

以下のように refine 後のメソッドを特異クラスを経由して private 化しようとするとエラーになるというバグ報告です。

module Nothing; end

class X
  # prepend しなかったらエラーにはならない
  prepend Nothing

  def hoge
  end
end

# これは OK
X.new.singleton_class.class_eval { private :hoge }

module NeverUsed
  refine X do
    def hoge(*keys)
    end
  end
end

# `private': undefined method `hoge' for class `#<Class:#<X:0x0000558fa95b7d70>>' (NameError)
# Refinements で拡張したあとに呼ぶとエラーになる
X.new.singleton_class.class_eval { private :hoge }

これは Ruby 3.0.1 で修正済みです。

[Bug #17488] Regression in Ruby 3: Hash#key? is non-deterministic when argument uses DelegateClass

次のように Ruby 3.0 で Hask#key?DelegateClass を渡すと意図しない結果が返ってくるというバグ報告です。

puts "Running on Ruby: #{RUBY_DESCRIPTION}"

program = <<~EOS
  require "delegate"
  TypeName = DelegateClass(String)

  hash = {
  "Int" => true,
  "Float" => true,
  "String" => true,
  "Boolean" => true,
  "WidgetFilter" => true,
  "WidgetAggregation" => true,
  "WidgetEdge" => true,
  "WidgetSortOrder" => true,
  "WidgetGrouping" => true,
  }

  puts hash.key?(TypeName.new("WidgetAggregation"))
EOS

iterations = 20
results = iterations.times.map { `ruby -e '#{program}'`.chomp }.tally

# Ruby 3.0 で実行すると false が返ってくることがある
puts "Results of checking `Hash#key?` #{iterations} times: #{results.inspect}"
# Ruby 2.7 => Results of checking `Hash#key?` 20 times: {"true"=>20}
# Ruby 3.0 => Results of checking `Hash#key?` 20 times: {"false"=>12, "true"=>8}

この問題は Ruby 3.0.1 で修正済みです。

[Bug #17481] Keyword arguments change value after calling super without arguments in Ruby 3.0

次のように super を呼び出す前と後でキーワード引数の値が変わってしまうというバグ報告です。

class BaseTest
  def call(a:, b:, **)
  end
end

class Test < BaseTest
  def call(a:, b:, **options)
  p options  # =>  {:c=>{}}
  super
  # super を呼び出した後で options の値が変わってしまっている…
  p options  # =>  {:c=>{}, :a=>1, :b=>2}
  end
end

Test.new.call(a: 1, b: 2, c: {})

この問題は Ruby 3.1 で修正済みです。
これ、Ruby 3.0 系にバックポートしなくても大丈夫なんだろうか…。

おわりに

と、言うことで25日続けた 一人 bugs.ruby Advent Calendar 2021 もこれにて完走です。
最初はやろうかどうしようか迷っていたんですがなんだかんだ今年の Ruby を振り返る事ができて楽しかったです。
開発者の皆様、今年も1年間お疲れ様でした&ありがとうございました。

【一人 bugs.ruby Advent Calendar 2021】[Bug #18396] An unexpected "hash value omission" syntax error when without parentheses call expr follows【24日目】

一人 bugs.ruby Advent Calendar 2021 24日目の記事になります。
今日は Ruby 3.1 で入る予定の Hash の値の省略記法で意図しない挙動がある話しです。

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

Ruby 3.1 では Hash の値の省略記法が新しく入りました。

name = "homu"

# pp(name: name) と同じ意味
pp name:
# => {:name=>"homu"}

これを使った時に次のようなコードで『次の行の式』が値になってしまうというバグ報告です。

name = "homu"

# func(name: name) になってほしいが実際には func(name: 42) となってしまう
pp name:
42
# => {:name=>42}

れ自体は Ruby 3.0 でも以下のように動作するのでこれは仕様との事です。

foo key:
bar

このようなケースでは以下のように明示的に () を付けて回避する必要があります。

name = "homu"

# pp(name: name) と同じ意味
pp(name:)
# => {:name=>"homu"}
42

意図せずうっかり書いちゃいそうなので明示的に () を付けて書くようにするとよさそうですね。
また、この挙動は matz 的には改善したいようなので Ruby 3.1 以降でまた挙動が変わるかもしれませんね。