【Ruby 3.0 Advent Calendar 2020】今年 Ruby に投げたパッチまとめ【6日目】

Ruby 3.0 Advent Calendar 2020 6日目の記事になります。
昨日は Rubyにはオブジェクトを汚染する仕組みがあった - いまブログです。

今年は RubyVM::AbstractSyntaxTree を触ることが多かったのですが、それに関連してバグっぽい挙動をいくつか見つけたのでその修正パッチを投げました。
今回はその修正内容についてまとめてみたいと思います。

[Bug #17013] RubyVM::AbstractSyntaxTree.parse("struct.field += foo") has no operator

Ruby 2.7.1 で struct.field += foo というコードを RubyVM::AbstractSyntaxTree構文解析すると以下のような結果になります。

pp RubyVM::AbstractSyntaxTree.parse("struct.field += foo").children.last
# => (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field (VCALL@1:16-1:19 :foo))

この時に OP_ASGN+=演算子情報が欠落してしまっています。
なので struct.field += foo でも struct.field -= foo でも同じ AST になってしまいます。

pp RubyVM::AbstractSyntaxTree.parse("struct.field += foo").children.last
# => (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field (VCALL@1:16-1:19 :foo))

pp RubyVM::AbstractSyntaxTree.parse("struct.field -= foo").children.last
# => (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field (VCALL@1:16-1:19 :foo))

このチケットでは +=-= であることが分かるように演算子を AST 情報に含めるように修正しました。

# 修正後
# :+ 情報が含まれる
pp RubyVM::AbstractSyntaxTree.parse("struct.field += foo").children.last
# => (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field :+ (VCALL@1:16-1:19 :foo))

# :- 情報が含まれる
pp RubyVM::AbstractSyntaxTree.parse("struct.field -= foo").children.last
# => (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field :- (VCALL@1:16-1:19 :foo))

この修正 PR は無事にマージされました。

[Bug #17015] RubyVM::AbstractSyntaxTree.parse has the same result on proc { |a| } and proc { |a,| }

Ruby では proc { |a| }proc { |a,| } ではちょっと意味が違います。

# 配列をそのまま受け取る
p proc { |a| a }.call [1, 2]
# => [1, 2]

# 配列を展開して受け取る
p proc { |a,| a }.call [1, 2]
# => 1

しかし、 RubyVM::AbstractSyntaxTree構文解析の結果は両方共同じ値になっていました。

pp RubyVM::AbstractSyntaxTree.parse("proc { |a| }").children.last
# => (ITER@1:0-1:12 (FCALL@1:0-1:4 :proc nil)
#       (SCOPE@1:5-1:12
#        tbl: [:a]
#        args:
#          (ARGS@1:8-1:9
#           pre_num: 1
#           pre_init: nil
#           opt: nil
#           first_post: nil
#           post_num: 0
#           post_init: nil
#           rest: nil
#           kw: nil
#           kwrest: nil
#           block: nil)
#        body: (BEGIN@1:10-1:10 nil)))

pp RubyVM::AbstractSyntaxTree.parse("proc { |a,| }").children.last
# => (ITER@1:0-1:13 (FCALL@1:0-1:4 :proc nil)
#       (SCOPE@1:5-1:13
#        tbl: [:a]
#        args:
#          (ARGS@1:8-1:10
#           pre_num: 1
#           pre_init: nil
#           opt: nil
#           first_post: nil
#           post_num: 0
#           post_init: nil
#           rest: nil
#           kw: nil
#           kwrest: nil
#           block: nil)
#        body: (BEGIN@1:11-1:11 nil)))

この問題を解消するために proc { |a,| } の場合は :NODE_SPECIAL_EXCESSIVE_COMMA という専用のフラグを追加しました。

pp RubyVM::AbstractSyntaxTree.parse("proc { |a,| }").children.last
# => (ITER@1:0-1:13 (FCALL@1:0-1:4 :proc nil)
#       (SCOPE@1:5-1:13
#        tbl: [:a]
#        args:
#          (ARGS@1:8-1:10
#           pre_num: 1
#           pre_init: nil
#           opt: nil
#           first_post: nil
#           post_num: 0
#           post_init: nil
#           rest: :NODE_SPECIAL_EXCESSIVE_COMMA   # これを追加
#           kw: nil
#           kwrest: nil
#           block: nil)
#        body: (BEGIN@1:11-1:11 nil)))

これで proc { |a| } なのか proc { |a,| } なのかを AST 上で判定する事ができます。

おわりに

と、言うことでニッチなバグ修正ですが少し Ruby に貢献できたかなと思います。
RubyVM::AbstractSyntaxTree は今年かなり使い込んでいて今回修正していたバグはかなり致命的でした。
実際困っていたのでそれがモチベーションになりパッチを書いて修正まで持っていけたのかな、と思います。
皆さんも Ruby で困っている事があればどんどん意見を出していけばいいと思います。
あと書いてて気づいたんですがこの2つのバグ修正は Ruby 2.7.2 にバックポートされたので Ruby 3.0 とは全然関係ありませんでした。おわり。