RubyKaigi 2018の2日目、15:50に"RNode with code positions"というお話をします。当日どういった話をするか、書いておきたいと思います。
表題にある"RNode"。これはRubyのAST(Abstract Syntax Tree: 抽象構文木)のためのデータ構造(構造体)のことです。"code position(s)"というのは、その名の通り、コードに関する位置情報のことです。
実はRuby 2.5からRubyが内部的に保有する位置情報が増えました。 お手元のRuby 2.5と2.4で以下のコードを実行して、その結果を比較してみてください。
$ ruby --dump=p -e '"str".upcase'
おそらく、こんな差分が出るとおもいます。
@@ -1,8 +1,8 @@
# +- nd_body:
-# | @ NODE_CALL (line: 1)
+# | @ NODE_CALL (line: 1, code_range: (1,0)-(1,12))
# | +- nd_mid: :upcase
# | +- nd_recv:
-# | | @ NODE_STR (line: 1)
+# | | @ NODE_STR (line: 1, code_range: (1,0)-(1,5))
# | | +- nd_lit: "str"
# | +- nd_args:
# | (null node)
Ruby 2.5系ではcode_range: (1,0)-(1,12)
という情報が増えていることが分かります。
上の例では
"str"
というのがNODE_STR
(文字列リテラル)で、それは1行目の0番目から、1行目の5番目の部分に相当する"str".upcase
というのがNODE_CALL
(メソッド呼び出し)で、それは1行目の0番目から、1行目の12番目の部分に相当する
ということがわかります。 行番号の情報しか保有していなかったRuby 2.4系に比べて、Ruby 2.5系では開始位置/終了位置の行番号/オフセット情報(カラム番号)を保有するようになっています。
このような情報が必要になった背景には、同じくRuby 2.5から導入された
- Branch coverage
- Method coverage
という機能があります。
Branch coverageというのは、その名のとおり分岐に関するカバレッジです。
def eq_zero(i)
(i == 0) ? :t : :f
end
というコードは分岐を1つ含んでいます。このメソッドのテストがあったとして、そのテストを評価するとき、例えば:t
を返すケースと、:f
を返すケースの両方があるかどうかを確認したいとしましょう。Ruby 2.4までの場合、"ある行を実行したか"というカバレッジしかとれませんでした。なので、eq_zero(0)
のケースがテストされているだけで、このメソッドの行単位でのカバレッジは 1/1、つまり100%になってしまうわけです。Branch coverageとして計測すれば、then
の方しか実行されていないことがわかるので 1/2、つまり50%であることがわかるという仕組みです。
coverageに関する詳しい情報は 遠藤さんの去年の発表 をみてください。
Branch coverageに関して、細かい位置情報がどうして必要になるかというと、それは以下の2つの理由によります。1つめはガバレッジ測定結果の可視化に関するものです。
def eq_zero(i)
(i == 0) ? :t : :f
| +------ 0回
+---------- 1回
end
というように可視化する場合に、then
とelse
に該当するコードの範囲がわかると、可視化が簡単になります。
2つめは1行に複数の分岐を書くことができるためです。
(a == b) ? ((c == d) ? :A : :B) : :C
というコードは1行に2つの分岐をふくんでいます。1つめのthen
(((c == d) ? :A : :B)
)と2つめのthen
(:A
)を区別するときに、該当するコードの範囲として表現できると、どの分岐の話をしているか明確になります。
頑張って実装しました。という冗談はさておき、ここはそれなりに長い話になるので、ぜひ当日発表を聞きにきてください。 この話をする際には、どうしても"(悪魔城)parse.y"とか"bison"とか、ちょっとだけVMとかの話に触れる必要があります。
- Rubyソースコード完全解説 の"第 2 部「構文解析」"
- Rubyのしくみ Ruby Under a Microscope
などを一読しておくと、いろいろ捗るかと思います。
いくつか応用できる場所は考えており、その話も当日したいと思います。
一例をあげると、NoMethodError
のメッセージをより詳しくするということが考えられます。
class A
def foo
nil
end
end
A.new.foo.foo
こういうコードを実行した時に、以下のようなメッセージをだすことを考えています。
Traceback (most recent call last):
/tmp/test.rb:7:in `<main>': undefined method `foo' for nil:NilClass (NoMethodError)
A.new.foo.foo
^^^^
PoCの実装はGitHubにあげてあります。Buildして、上記のコードを適当な名前のファイル(例えば"test.rb")にコピーしてもらって、以下のように実行していただくことができるはずです。
手を抜いてno_method_error_ext.rb
でNoMethodError
を拡張しているのがばればれですね。。。
$ ruby -r./no_method_error_ext.rb test.rb
この辺を触り始めたのは、去年のRubyKaigiに向かう新幹線のなかでした。行きの新幹線でRNode
構造体にnd_reserved
という使われていなさそうなメンバーをみつけ、これをうまく使えないのかなぁと考えていました。その後広島で笹田さんや中田さんに、"これ空いてますよね? 使ってみていいですか?"という話をした記憶があります(たしか)。遠藤さんの発表で"だれかこのあたり実装してみてくれませんか"という呼びかけもあり、広島から戻ってから"parse.y"をいじるようになりました。
去年のRubyKaigiをきっかけに実装を始めた機能について、今年のRubyKaigiでお話ができるということで、個人的には感慨深くおもっています。
それでは仙台でお会いしましょう。