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

今週は Enumerable#take_while_afterEnumerable#detect_only といったメソッドの提案がありました

[Feature #18136] take_while_after

tokens = [
  {text: 'Ruby', type: :word},
  {text: 'is', type: :word},
  {text: 'cool', type: :word},
  {text: '.', type: :punctuation, ends_sentence: true},
  {text: 'Rust', type: :word},
  # ...
]

# _1[:ends_sentence] が真になるまでの要素を返す
sentence = tokens.take_while_after { _1[:ends_sentence] }
pp sentence
# => [{:text=>"Ruby", :type=>:word},
#     {:text=>"is", :type=>:word},
#     {:text=>"cool", :type=>:word},
#     {:text=>".", :type=>:punctuation, :ends_sentence=>true}]

# これは slice_after.first と同じ挙動
sentence = tokens.slice_after { _1[:ends_sentence] }.first
pp sentence
# => [{:text=>"Ruby", :type=>:word},
#     {:text=>"is", :type=>:word},
#     {:text=>"cool", :type=>:word},
#     {:text=>".", :type=>:punctuation, :ends_sentence=>true}]

[Feature #18135] Introduce Enumerable#detect_only

  • #detect で要素が見つからなかった場合に例外を返す Enumerable#detect_only メソッドの提案ぽい?
  • #detect で要素が1個だけ見つかった場合は成功し、それ以外は例外を返す Enumerable#detect_only メソッドの提案
  • 次のようなコードを
matches = array.select { |elem| some_method(elem) }
raise if matches.size != 1
match = matches.first
  • 以下のようにかけるようにする提案らしい
match = array.detect_only { |elem| some_method(elem) }

[Bug #18126] Process termination three seconds after thread termination dumps core

  • 次のようなコードを実行するとランダムで segv することがあるらしい
3000.times{Thread.new{}}
sleep 2.99
$ ruby test.rb
[BUG] Segmentation fault at 0x0000000000000440
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-linux]

$
  • これは
    • Ruby のスレッドが終了し、3分以内に新しい Ruby のスレッドが作成されると、その内部の pthread が別のRuby のスレッドによって再利用される
    • しかし、pthread が3分待機している間に Ruby のプロセスプロセスが RubyVM と GC が破棄される
    • その後、xfreeは使用できなくなり、セグメンテーション違反が発生する
  • らしい

[Misc #18125] A strange behavior when same name variable/method coexist issue.

  • 次のように同名のメソッドと変数を定義した時に奇妙な動作になるけどこれは期待する動作?という質問
def deploy_to
  "deploy_to"
end

deploy_to = "#{deploy_to} new place"
#              ^ これがメソッドではなくて変数を参照している

p defined? deploy_to # => local_varible
p deploy_to  # => " new place"
  • これは期待する動作でこの場合は変数が定義され値が割り当てられる前のローカル変数 deploy_to を参照する
  • メソッドを参照したい場合は () を付けてメソッド呼び出しだと明示化する必要がある
def deploy_to
  "deploy_to"
end

deploy_to = "#{deploy_to()} new place"
#              ^ これはメソッド呼び出しになる
p deploy_to  # => "deploy_to new place"

RubyKaigi Takeout 2021 で Ruby のマクロについて話します

先日 RubyKaigi Takeout 2021 のスピーカーとスケジュールが発表されました。
わたしも 3日目の朝一に Use Macro all the time ~ マクロを使いまくろ ~というタイトルで登壇させていただきます。
内容はタイトルにも書かれている通りマクロの話をします。
マクロと言ってもいわゆる C言語のようなプリプロセスマクロではなくて『AST レベルで Ruby の構文を別の構文へと変換すること』を Ruby のマクロと定義し、Ruby でどのようにマクロを表現するのか、みたいな話をする予定です。
まあもっとさっくりう言うと『Ruby でマクロを実装してみたらこうなりました』みたいな感じですかねえ。
例えばマクロを使用することで、

hoge.foo.bar

のようなコードを

hoge&.foo&.bar

のようにぼっち演算子呼び出しとして変換する事ができたり、

CONST_HOGE = [1, 2, 3]

というような定数定義しているコードを

CONST_HOGE = [1, 2, 3].freeze

のように暗黙的に freeze したり、

![a, b, c]

というようなコードを

{ a: a, b: b, c: c }

みたいな Hash に展開するようなことを実現することができます。
Ruby のマクロは以下のような過程を経て Ruby のコードから別の Ruby のコードへと変換しています。

開発中のコードをちょっとだけお見せすると Ruby のマクロを使うことによって以下のようなことを実現することができます。

# ※開発中のコードなので当日までに変わるかもね!
using Macro::Refine::Source


# 特定の構文を変換するマクロを定義する
module MyMacro
  using Macro::Macroable

  # cat! というメソッド呼び出しを任意のコードに変換する
  def cat!(num = ast { 1 })
    ast { "にゃーん" * $num }
  end
  macro_function :cat!

  # 範囲リテラルを freeze するコードに変換する
  def freezing(node, parent)
    ast { $node.freeze }
  end
  macro_node :DOT2, :freezing
  macro_node :DOT3, :freezing

  # 代入式を freeze するコードに変換する
  def assign_freezing(node, name:, value:)
    ast { $name = $value.freeze }
  end
  macro_pattern pat { $name = $value }, :assign_freezing
end


# 変換したい Ruby のコードを Proc で定義
body = proc {
  use_macro! MyMacro

  puts cat!
  puts cat!(3)

  value = [2, 3, 5, 7, 11, 13, 17].grep((0..10))

  HOGE = "にゃーん" + "にゃーん"
}

# マクロを適用させた AST に変換する
result = Macro.compile_of(body)

# 変換させた AST から Ruby のソースコードを取得する
pp result.source

# 以下のようなコードに変換される
__END__

# cat!(n) が "にゃーん" * n に変換される
puts "にゃーん" * 1
puts "にゃーん" * 3

# (0..10) が (0..10).freeze に変換される
# value = hoge が value = hoge.freeze に変換される
value = [2, 3, 5, 7, 11, 13, 17].grep((0..10).freeze).freeze

HOGE = ("にゃーん" + "にゃーん").freeze

上記のような書き方で Ruby のコードを AST レベルで別のコードへと変換するような実装を書いています。
ちなみに実装はすべて Ruby で行っているので MRI 側で特に何か機能を追加したりとかはしていません。
この登壇でメタプロの向こう側ってやつを見せてやりますよ(誇張。
と、言うことでこういう話が好きな方は当日までお楽しみに! ちなみに去年を除くとこれが RubyKaigi 初参加になります。
RubyKaigi 初参加で初登壇の実績を解除したぜ…。

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

今週は右代入で特定のケースでシンタックスエラーになるというバグ報告がありました。

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

  • 次のように再帰的な HashJSON.dump に渡すと VM がクラッシュするバグ報告
    • どうやら Linux 系でのみ発生しているぽい?
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)
  • これが期待する動作なのかどうか、という旨がチケットに書かれていたが最新版では 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
#     ^~
  • このエラーは意図的ではないが修正するのは難しいらしい

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

今週は instance_execMethod#to_proc を渡すと意図しない例外が返ってくるというバグ報告や Ruby 3.0.2 で Hash#transform_keys! を呼び出すとメモリリークをするという報告がありました。

[Feature #18070] attr should be removed

  • 1.9 から非推奨だった attr を消すチケット
  • attr は以下のようなメソッド
class User
  # getter のみ定義される
  attr :id

  # true を渡すと setter も定義される
  attr :name, true
end
  • 非推奨なのは第二引数の true / false を渡すことなので attr 自体は特に問題なさそう?

[Feature #1806] instance_exec is just ignored when the block is originally a method

  • Method#to_proc の結果は元のコンテキスト情報を持っているので次のようなコードは失敗する
f = -> (x) { a + x }

class A
  def a
    1
  end
end

# これは f 内の a は A#a を参照する
A.new.instance_exec(1, &f) # => 2


class B
  def b(x)
    a + x
  end
end

# B#method の場合の場合は a が B#a を参照する
proc = B.new.method(:b).to_proc
# なのでエラーになる
A.new.instance_exec(1, &proc)
# => undefined local variable or method `a' for #<B:0x00007fdaf30480a0> (NameError)
  • このチケットの提案自体は NameError になるのではなくて ArgumentError になるべきでは?という内容
  • これ自体は期待する挙動らしい
  • なぜなら Method#to_proc の実装は以下のようになっており結果的に Method#call を呼んでいるから
class Method
  def to_proc
    method = self
    ->(*args, **kwargs, &block) do
      # ここで `B#b` が呼ばれるため結果的に NameError になる
      method.call(*args, **kwargs, &block)
    end
  end
end

[Bug #18065] 3.0.2 - possible memory leak in Hash#transform_keys!

  • 次のようなコードを Ruby 3.0.2 で実行するとメモリリークしてメモリを大量に消費するらしい
    • これは Ruby 3.0.2 で再現して Ruby 3.0.1 では問題ない
h = { value1: 1, value2: 2 }
loop { h.transform_keys!(&:to_s) }
  • この問題は既に最新版で修正済み

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

今週はネストしたループで caller_locations を呼び出すと segv するバグ報告などがありました。

[Feature #18057] Introduce Array#mean

  • 配列の平均値を求める Array#mean メソッドを追加する提案
    • 元々は Array#average という名前だったが #mean という名前に変わった
array = [1, 2, 3]
array.mean # 2

# 要素の値を変更しながら適用
array = [1.5, 2.2, 3.1]
array.mean(&:round) # 2.3333333333333335

array = [-3, -2, -1]
array.mean { |e| e.abs } # 2

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

  • 以下のコードを実行すると segv するというバグ報告
    • Ruby 3.0 以降で再現する
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 の最適化のバグっぽい?
  • 修正 PR は既に出てる
  • Ruby 3.0 に関しては最適化を元に戻すのが最善らしい
    • 実装が複雑になっているので今回のバグを対応する場合は更に実装が複雑になるとのこと
    • ただし、最適化を維持したい人がいれば別途対応するとのこと
    • https://bugs.ruby-lang.org/issues/18053#note-1

[Bug #18052] Find のignore_error オプションが、文字化けファイル遭遇時の例外に対応していない (Windows)

  • Find.find(".", ignore_error: true) のように ignore_error: true を指定しても文字化けしたファイルがあると例外が発生するらしい
# coding:cp932
p __ENCODING__

# cp932 では表現できないファイル名を作る(例はハングル文字)。  
open("testfile-\uD7A3 .jpg", "w")

require "find"
Find.find(".", ignore_error: true ) {|f|
    p f
}
# => #<Encoding:Windows-31J>
#    "."
#    "./testfile-???R?s?[.jpg"
#    Traceback (most recent call last):
#            25: from d:/opt/ruby/bin/irb:23:in `<main>'
#            24: from d:/opt/ruby/bin/irb:23:in `load'
#            23: from d:/opt/ruby/lib/ruby/gems/2.6.0/gems/irb-1.3.2/exe/irb:11:in `<top (required)>'
#            6: from (irb):10:in `<main>'
#            5: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:43:in `find'
#            4: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:43:in `each'
#            3: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:48:in `block in find'
#            2: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:48:in `catch'
#            1: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:51:in `block (2 levels) in find'
#    d:/opt/ruby/lib/ruby/2.6.0/find.rb:51:in `lstat': Invalid argument @ rb_file_s_lstat - ./testfile-?[.jpg (Errno::EINVAL)

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 が立ってるけど特に進展がなさそう